Complete agent parent (#6049)

* add role and tools filling

* add: file-upload

---------

Co-authored-by: xxyyh <2289112474@qq>
This commit is contained in:
YeYuheng 2025-12-08 12:59:43 +08:00 committed by GitHub
parent 7bd1a8b654
commit 3fd837c221
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 396 additions and 60 deletions

View File

@ -1,4 +1,4 @@
import { ChatCompletionRequestMessageRoleEnum } from 'core/ai/constants';
import { ChatCompletionRequestMessageRoleEnum } from '../../ai/constants';
import type {
ChatCompletionContentPart,
ChatCompletionFunctionMessageParam,
@ -96,7 +96,7 @@ export const helperChats2GPTMessages = ({
tool_calls
});
aiResults.push(...toolResponse);
} else if ('text' in value && value.text?.content === 'string') {
} else if ('text' in value && typeof value.text?.content === 'string') {
if (!value.text.content && item.value.length > 1) {
return;
}

View File

@ -78,11 +78,22 @@ export type HelperBotChatItemSiteType = z.infer<typeof HelperBotChatItemSiteSche
/* 具体的 bot 的特有参数 */
// AI 模型配置
export const AIModelConfigSchema = z.object({
model: z.string(),
temperature: z.number().nullish(),
maxToken: z.number().nullish(),
stream: z.boolean().nullish()
});
export type AIModelConfigType = z.infer<typeof AIModelConfigSchema>;
export const topAgentParamsSchema = z.object({
role: z.string().nullish(),
taskObject: z.string().nullish(),
selectedTools: z.array(z.string()).nullish(),
selectedDatasets: z.array(z.string()).nullish(),
fileUpload: z.boolean().nullish()
fileUpload: z.boolean().nullish(),
// AI 模型配置
modelConfig: AIModelConfigSchema.nullish()
});
export type TopAgentParamsType = z.infer<typeof topAgentParamsSchema>;

View File

@ -17,7 +17,9 @@ export enum SseResponseEventEnum {
interactive = 'interactive',
agentPlan = 'agentPlan' // agent plan
agentPlan = 'agentPlan', // agent plan
formData = 'formData' // form data for TopAgent
}
export enum DispatchNodeResponseKeyEnum {

View File

@ -1,35 +1,207 @@
import type { HelperBotDispatchParamsType, HelperBotDispatchResponseType } from '../type';
import { helperChats2GPTMessages } from '@fastgpt/global/core/chat/helperBot/adaptor';
import { getPrompt } from './prompt';
import { createLLMResponse } from '../../../../ai/llm/request';
import { getLLMModel } from '../../../../ai/model';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/helperBot/type';
import { getSystemToolsWithInstalled } from '../../../../app/tool/controller';
export const dispatchTopAgent = async (
props: HelperBotDispatchParamsType
): Promise<HelperBotDispatchResponseType> => {
const { query, files, metadata, histories } = props;
const messages = helperChats2GPTMessages({
const { query, files, metadata, histories, workflowResponseWrite, teamId, userId } = props;
const modelConfig = metadata.data?.modelConfig;
const modelName = modelConfig?.model || global.systemDefaultModel?.llm?.model;
if (!modelName) {
throw new Error('未配置 LLM 模型,请在前端选择模型或在系统中配置默认模型');
}
const modelData = getLLMModel(modelName);
if (!modelData) {
throw new Error(`模型 ${modelName} 未找到`);
}
const temperature = modelConfig?.temperature ?? 0.7;
const maxToken = modelConfig?.maxToken ?? 4000;
const stream = modelConfig?.stream ?? true;
const resourceList = await generateResourceList({
teamId,
userId
});
const historyMessages = helperChats2GPTMessages({
messages: histories,
reserveTool: false
});
// 拿工具资源参考 FastGPT/projects/app/src/pages/api/core/app/tool/getSystemToolTemplates.ts
/*
onReasoning({ text }) {
if (!aiChatReasoning) return;
workflowStreamResponse?.({
const systemPrompt = getPrompt({ resourceList });
const conversationMessages = [
{ role: 'system' as const, content: systemPrompt },
...historyMessages,
{ role: 'user' as const, content: query }
];
// console.log('📝 TopAgent 阶段 1: 信息收集');
// console.log('conversationMessages:', conversationMessages);
const llmResponse = await createLLMResponse({
body: {
messages: conversationMessages,
model: modelName,
temperature,
stream,
max_tokens: maxToken
},
onStreaming: ({ text }) => {
workflowResponseWrite?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({ text })
});
},
onReasoning: ({ text }) => {
workflowResponseWrite?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({ reasoning_content: text })
});
}
});
const firstPhaseAnswer = llmResponse.answerText;
const firstPhaseReasoning = llmResponse.reasoningText;
// 尝试解析信息收集阶段的 JSON 响应
let parsedResponse: { reasoning?: string; question?: string } | null = null;
try {
parsedResponse = JSON.parse(firstPhaseAnswer);
} catch (e) {
// 如果解析失败,说明不是 JSON 格式,可能是普通文本
parsedResponse = null;
}
if (firstPhaseAnswer.includes('「信息收集已完成」')) {
console.log('🔄 TopAgent: 检测到信息收集完成信号,切换到计划生成阶段');
const newMessages = [
...conversationMessages,
{ role: 'assistant' as const, content: firstPhaseAnswer },
{ role: 'user' as const, content: '请你直接生成规划方案' }
];
// console.log('📋 TopAgent 阶段 2: 计划生成');
const planResponse = await createLLMResponse({
body: {
messages: newMessages,
model: modelName,
temperature,
stream,
max_tokens: maxToken
},
onStreaming: ({ text }) => {
workflowResponseWrite?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
reasoning_content: text
})
data: textAdaptGptResponse({ text })
});
},
onStreaming({ text }) {
if (!isResponseAnswerText) return;
workflowStreamResponse?.({
onReasoning: ({ text }) => {
workflowResponseWrite?.({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text
})
data: textAdaptGptResponse({ reasoning_content: text })
});
}
*/
});
let formData;
try {
const planJson = JSON.parse(planResponse.answerText);
// console.log('解析的计划 JSON:', planJson);
formData = {
role: planJson.task_analysis?.role || '',
taskObject: planJson.task_analysis?.goal || '',
tools: planJson.resources?.tools?.map((tool: any) => tool.id) || [],
fileUploadEnabled: planJson.resources?.system_features?.file_upload?.enabled || false
};
} catch (e) {
console.error('解析计划 JSON 失败:', e);
}
return {
aiResponse: formatAIResponse(planResponse.answerText, planResponse.reasoningText),
formData
};
}
const displayText = parsedResponse?.question || firstPhaseAnswer;
return {
aiResponse: formatAIResponse(displayText, firstPhaseReasoning)
};
};
const generateResourceList = async ({
teamId,
userId
}: {
teamId: string;
userId: string;
}): Promise<string> => {
let result = '\n## 可用资源列表\n';
const tools = await getSystemToolsWithInstalled({
teamId,
isRoot: true // TODO: 需要传入实际的 isRoot 值
});
const installedTools = tools.filter((tool) => {
return tool.installed && !tool.isFolder;
});
if (installedTools.length > 0) {
result += '### 工具\n';
installedTools.forEach((tool) => {
const toolId = tool.id;
const name =
typeof tool.name === 'string'
? tool.name
: tool.name?.en || tool.name?.['zh-CN'] || '未命名';
const intro =
typeof tool.intro === 'string' ? tool.intro : tool.intro?.en || tool.intro?.['zh-CN'] || '';
const description = tool.toolDescription || intro || '暂无描述';
result += `- **${toolId}** [工具]: ${name} - ${description}\n`;
});
} else {
result += '### 工具\n暂无已安装的工具\n';
}
// TODO: 知识库
result += '\n### 知识库\n暂未配置知识库\n';
result += '\n### 系统功能\n';
result += '- **file_upload**: 文件上传功能 (enabled, purpose, file_types)\n';
return result;
};
const formatAIResponse = (text: string, reasoning?: string): AIChatItemValueItemType[] => {
const result: AIChatItemValueItemType[] = [];
if (reasoning) {
result.push({
reasoning: {
content: reasoning
}
});
}
result.push({
text: {
content: text
}
});
return result;
};

View File

@ -253,9 +253,11 @@ ${resourceList}
- resources
****
**JSON**
****
1. JSON规定的字段
2.
JSON
JSON
{
"task_analysis": {
"goal": "任务的核心目标描述",
@ -353,7 +355,7 @@ ${resourceList}
- 使 tools 使 resources
-
- 使 resources toolsknowledgessystem_features
- file_upload.enabled=true purpose
- file_upload.enabled=true purpose
- knowledges tools
- JSON内容

View File

@ -11,11 +11,21 @@ export const HelperBotDispatchParamsSchema = z.object({
files: HelperBotCompletionsParamsSchema.shape.files,
metadata: HelperBotCompletionsParamsSchema.shape.metadata,
histories: z.array(HelperBotChatItemSchema),
workflowResponseWrite: WorkflowResponseFnSchema
workflowResponseWrite: WorkflowResponseFnSchema,
teamId: z.string(),
userId: z.string()
});
export type HelperBotDispatchParamsType = z.infer<typeof HelperBotDispatchParamsSchema>;
export const HelperBotDispatchResponseSchema = z.object({
aiResponse: z.array(AIChatItemValueItemSchema)
aiResponse: z.array(AIChatItemValueItemSchema),
formData: z
.object({
role: z.string().optional(),
taskObject: z.string().optional(),
tools: z.array(z.string()).optional(),
fileUploadEnabled: z.boolean().optional()
})
.optional()
});
export type HelperBotDispatchResponseType = z.infer<typeof HelperBotDispatchResponseSchema>;

View File

@ -7,7 +7,7 @@ import type {
} from '@fastgpt/global/core/chat/type';
import { MongoHelperBotChatItem } from './chatItemSchema';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { mongoSessionRun } from 'common/mongo/sessionRun';
import { mongoSessionRun } from '../../../common/mongo/sessionRun';
export const pushChatRecords = async ({
type,

View File

@ -453,6 +453,8 @@
"toolkit_uninstalled": "未安装",
"toolkit_update_failed": "更新失败",
"toolkit_user_guide": "使用说明",
"tool_load_failed": "部分工具加载失败",
"failed_tools": "失败的工具",
"tools": "工具",
"tools_no_description": "这个工具没有介绍~",
"tools_tip": "声明模型可用的工具,可以实现与外部系统交互等扩展能力",

View File

@ -34,7 +34,7 @@ import { streamFetch } from '@/web/common/api/fetch';
import type { generatingMessageProps } from '../ChatContainer/type';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
const ChatBox = ({ type, metadata, ...props }: HelperBotProps) => {
const ChatBox = ({ type, metadata, onApply, ...props }: HelperBotProps) => {
const { toast } = useToast();
const { t } = useTranslation();
@ -144,7 +144,19 @@ const ChatBox = ({ type, metadata, ...props }: HelperBotProps) => {
}
);
const generatingMessage = useMemoizedFn(
({ event, text = '', reasoningText, tool }: generatingMessageProps) => {
({ event, text = '', reasoningText, tool, data }: generatingMessageProps & { data?: any }) => {
if (event === SseResponseEventEnum.formData && data) {
const formData = {
role: data.role || '',
taskObject: data.taskObject || '',
selectedTools: data.tools || [],
selectedDatasets: [],
fileUpload: data.fileUploadEnabled || false
};
onApply?.(formData);
return;
}
setChatRecords((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
@ -276,6 +288,7 @@ const ChatBox = ({ type, metadata, ...props }: HelperBotProps) => {
const chatItemDataId = getNanoid(24);
const newChatList: HelperBotChatItemSiteType[] = [
...chatRecords,
// 用户消息
{
_id: getNanoid(24),
createTime: new Date(),
@ -289,6 +302,7 @@ const ChatBox = ({ type, metadata, ...props }: HelperBotProps) => {
}
]
},
// AI 消息 - 空白,用于接收流式输出
{
_id: getNanoid(24),
createTime: new Date(),
@ -297,20 +311,7 @@ const ChatBox = ({ type, metadata, ...props }: HelperBotProps) => {
value: [
{
text: {
content: `我无法直接通过“读取静态网页工具”获取 GitHub动态站点上的实时信息因此不能自动抓取 fastgpt 的 star 数量。
- **FastGPTfastgpt-dev/FastGPT**
GitHub https://github.com/fastgpt-dev/FastGPT
** star ** star
1. star 2025
2. GitHub API
3. `
content: ''
}
}
]
@ -340,7 +341,7 @@ const ChatBox = ({ type, metadata, ...props }: HelperBotProps) => {
})),
metadata: {
type: 'topAgent',
data: {}
data: metadata
}
},
onMessage: generatingMessage,

View File

@ -21,14 +21,19 @@ import type { Form2WorkflowFnType } from '../FormComponent/type';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import HelperBot from '@/components/core/chat/HelperBot';
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import { getToolPreviewNode } from '@/web/core/app/api/tool';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { useToast } from '@fastgpt/web/hooks/useToast';
type Props = {
appForm: AppFormEditFormType;
setAppForm?: React.Dispatch<React.SetStateAction<AppFormEditFormType>>;
setRenderEdit: React.Dispatch<React.SetStateAction<boolean>>;
form2WorkflowFn: Form2WorkflowFnType;
};
const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props) => {
const { t } = useTranslation();
const { toast } = useToast();
const [activeTab, setActiveTab] = useSafeState<'helper' | 'chat_debug'>('helper');
@ -58,6 +63,24 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
isReady: true
});
// 构建 TopAgent metadata,从 appForm 中提取配置
const topAgentMetadata = useMemo(
() => ({
role: appForm.aiSettings.aiRole,
taskObject: appForm.aiSettings.aiTaskObject,
selectedTools: appForm.selectedTools.map((tool) => tool.id),
selectedDatasets: appForm.dataset.datasets.map((dataset) => dataset.datasetId),
fileUpload: false, // TODO: 从配置中获取文件上传设置
modelConfig: {
model: appForm.aiSettings.model,
temperature: appForm.aiSettings.temperature,
maxToken: appForm.aiSettings.maxToken,
stream: true
}
}),
[appForm]
);
return (
<Flex h={'full'} gap={2}>
<MyBox
@ -110,7 +133,97 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
</Flex>
<Box flex={1}>
{activeTab === 'helper' && (
<HelperBot type={HelperBotTypeEnum.topAgent} metadata={{}} onApply={() => {}} />
<HelperBot
type={HelperBotTypeEnum.topAgent}
metadata={topAgentMetadata}
onApply={async (formData) => {
if (!setAppForm) {
console.warn('⚠️ setAppForm 未传入,无法更新表单');
return;
}
const existingToolIds = new Set(
appForm.selectedTools.map((tool) => tool.pluginId).filter(Boolean)
);
// console.log('📋 当前已存在的工具 pluginId:', Array.from(existingToolIds));
// console.log('📋 formData.selectedTools:', formData.selectedTools);
const newToolIds = (formData.selectedTools || []).filter(
(toolId: string) => !existingToolIds.has(toolId)
);
if (newToolIds.length === 0) {
// 没有新工具需要添加,仍然更新 role、taskObject 和文件上传配置
setAppForm((prev) => ({
...prev,
aiSettings: {
...prev.aiSettings,
aiRole: formData.role || '',
aiTaskObject: formData.taskObject || ''
},
chatConfig: {
...prev.chatConfig,
fileSelectConfig: {
...prev.chatConfig.fileSelectConfig,
canSelectFile: formData.fileUpload || false
}
}
}));
return;
}
let newTools: FlowNodeTemplateType[] = [];
const failedToolIds: string[] = [];
// 使用 Promise.allSettled 并行请求所有工具
const toolPromises = newToolIds.map((toolId: string) =>
getToolPreviewNode({ appId: toolId })
.then((tool) => ({ status: 'fulfilled' as const, toolId, tool }))
.catch((error) => ({ status: 'rejected' as const, toolId, error }))
);
const results = await Promise.allSettled(toolPromises);
results.forEach((result: any) => {
if (result.status === 'fulfilled' && result.value.status === 'fulfilled') {
newTools.push(result.value.tool);
} else if (result.status === 'fulfilled' && result.value.status === 'rejected') {
failedToolIds.push(result.value.toolId);
console.error(`❌ 工具 ${result.value.toolId} 获取失败:`, result.value.error);
}
});
if (failedToolIds.length > 0) {
toast({
title: t('app:tool_load_failed'),
description: `${t('app:failed_tools')}: ${failedToolIds.join(', ')}`,
status: 'warning',
duration: 5000
});
}
setAppForm((prev) => {
const newForm: AppFormEditFormType = {
...prev,
aiSettings: {
...prev.aiSettings,
aiRole: formData.role || '',
aiTaskObject: formData.taskObject || ''
},
selectedTools: [...prev.selectedTools, ...newTools],
chatConfig: {
...prev.chatConfig,
fileSelectConfig: {
...prev.chatConfig.fileSelectConfig,
canSelectFile: formData.fileUpload || false
}
}
};
return newForm;
});
}}
/>
)}
{activeTab === 'chat_debug' && <ChatContainer />}
</Box>
@ -128,7 +241,7 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
);
};
const Render = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
const Render = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props) => {
const { chatId } = useChatStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
@ -150,6 +263,7 @@ const Render = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest
appForm={appForm}
setAppForm={setAppForm}
setRenderEdit={setRenderEdit}
form2WorkflowFn={form2WorkflowFn}
/>

View File

@ -55,6 +55,7 @@ const Edit = ({
<Box flex={'2 0 0'} w={0} mb={3}>
<ChatTest
appForm={appForm}
setAppForm={setAppForm}
setRenderEdit={setRenderEdit}
form2WorkflowFn={agentForm2AppWorkflow}
/>

View File

@ -293,17 +293,18 @@ export const checkNeedsUserConfiguration = (toolTemplate: FlowNodeTemplateType):
[FlowNodeInputTypeEnum.timeRangeSelect]: true
};
return (
toolTemplate.inputs.length > 0 &&
toolTemplate.inputs.some((input) => {
// 有工具描述的不需要配置
if (input.toolDescription) return false;
// 禁用流的不需要配置
if (input.key === NodeInputKeyEnum.forbidStream) return false;
// 系统输入配置需要配置
if (input.key === NodeInputKeyEnum.systemInputConfig) return true;
((toolTemplate.inputs?.length ?? 0) > 0 &&
toolTemplate.inputs?.some((input) => {
// 有工具描述的不需要配置
if (input.toolDescription) return false;
// 禁用流的不需要配置
if (input.key === NodeInputKeyEnum.forbidStream) return false;
// 系统输入配置需要配置
if (input.key === NodeInputKeyEnum.systemInputConfig) return true;
// 检查是否包含表单类型的输入
return input.renderTypeList.some((type) => formRenderTypesMap[type]);
})
// 检查是否包含表单类型的输入
return input.renderTypeList.some((type) => formRenderTypesMap[type]);
})) ||
false
);
};

View File

@ -9,6 +9,7 @@ import { MongoHelperBotChatItem } from '@fastgpt/service/core/chat/HelperBot/cha
import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatch/utils';
import { dispatchMap } from '@fastgpt/service/core/chat/HelperBot/dispatch/index';
import { pushChatRecords } from '@fastgpt/service/core/chat/HelperBot/utils';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
export type completionsBody = HelperBotCompletionsParamsType;
@ -43,9 +44,19 @@ async function handler(req: ApiRequestProps<completionsBody>, res: ApiResponseTy
files,
metadata,
histories,
workflowResponseWrite
workflowResponseWrite,
teamId,
userId
});
// Send formData if exists
if (result.formData) {
workflowResponseWrite?.({
event: SseResponseEventEnum.formData,
data: result.formData
});
}
// Save chat
await pushChatRecords({
type: metadata.type,

View File

@ -50,6 +50,9 @@ type ResponseQueueItemType = CommonResponseType &
| SseResponseEventEnum.toolResponse;
tools: any;
}
| {
event: SseResponseEventEnum.formData;
}
);
class FatalError extends Error {}
@ -269,6 +272,12 @@ export const streamFetch = ({
event,
agentPlan: rest.agentPlan
});
} else if (event === SseResponseEventEnum.formData) {
// Directly call onMessage for formData, no need to queue
onMessage({
event,
data: rest
});
} else if (event === SseResponseEventEnum.error) {
if (rest.statusText === TeamErrEnum.aiPointsNotEnough) {
useSystemStore.getState().setNotSufficientModalType(TeamErrEnum.aiPointsNotEnough);