diff --git a/packages/global/core/chat/helperBot/adaptor.ts b/packages/global/core/chat/helperBot/adaptor.ts index f31f4e7e9..c4b68742a 100644 --- a/packages/global/core/chat/helperBot/adaptor.ts +++ b/packages/global/core/chat/helperBot/adaptor.ts @@ -10,11 +10,9 @@ import type { HelperBotChatItemType } from './type'; import { simpleUserContentPart } from '../adapt'; export const helperChats2GPTMessages = ({ - messages, - reserveTool = false + messages }: { messages: HelperBotChatItemType[]; - reserveTool?: boolean; }): ChatCompletionMessageParam[] => { let results: ChatCompletionMessageParam[] = []; @@ -66,29 +64,25 @@ export const helperChats2GPTMessages = ({ //AI: 只需要把根节点转化即可 item.value.forEach((value, i) => { - if ('tool' in value && reserveTool) { - const tool_calls: ChatCompletionMessageToolCall[] = [ - { - id: value.tool.id, - type: 'function', - function: { - name: value.tool.functionName, - arguments: value.tool.params - } - } - ]; - const toolResponse: ChatCompletionToolMessageParam[] = [ - { - tool_call_id: value.tool.id, - role: ChatCompletionRequestMessageRoleEnum.Tool, - content: value.tool.response - } - ]; - aiResults.push({ - role: ChatCompletionRequestMessageRoleEnum.Assistant, - tool_calls - }); - aiResults.push(...toolResponse); + if ('collectionForm' in value) { + const text = JSON.stringify( + value.collectionForm.params.inputForm.map((item) => ({ + label: item.label, + type: item.type + })) + ); + + // Concat text + const lastValue = item.value[i - 1]; + const lastResult = aiResults[aiResults.length - 1]; + if (lastValue && typeof lastResult?.content === 'string') { + lastResult.content += text; + } else { + aiResults.push({ + role: ChatCompletionRequestMessageRoleEnum.Assistant, + content: text + }); + } } else if ('text' in value && typeof value.text?.content === 'string') { if (!value.text.content && item.value.length > 1) { return; diff --git a/packages/global/core/chat/helperBot/type.ts b/packages/global/core/chat/helperBot/type.ts index ae9a09967..ea52faa39 100644 --- a/packages/global/core/chat/helperBot/type.ts +++ b/packages/global/core/chat/helperBot/type.ts @@ -2,6 +2,7 @@ import { ObjectIdSchema } from '../../../common/type/mongo'; import { z } from 'zod'; import { ChatRoleEnum } from '../constants'; import { UserChatItemSchema, SystemChatItemSchema, ToolModuleResponseItemSchema } from '../type'; +import { UserInputInteractiveSchema } from '../../workflow/template/system/interactive/type'; export enum HelperBotTypeEnum { topAgent = 'topAgent', @@ -34,7 +35,7 @@ export const AIChatItemValueItemSchema = z.union([ }) }), z.object({ - tool: ToolModuleResponseItemSchema + collectionForm: UserInputInteractiveSchema }) ]); export type AIChatItemValueItemType = z.infer; diff --git a/packages/global/core/workflow/runtime/constants.ts b/packages/global/core/workflow/runtime/constants.ts index 164eeeb6d..8fb47a65e 100644 --- a/packages/global/core/workflow/runtime/constants.ts +++ b/packages/global/core/workflow/runtime/constants.ts @@ -19,7 +19,9 @@ export enum SseResponseEventEnum { agentPlan = 'agentPlan', // agent plan - formData = 'formData', // form data for TopAgent + // Helperbot + collectionForm = 'collectionForm', // collection form for HelperBot + topAgentConfig = 'topAgentConfig', // form data for TopAgent generatedSkill = 'generatedSkill' // generated skill for SkillAgent } diff --git a/packages/service/core/chat/HelperBot/dispatch/topAgent/index.ts b/packages/service/core/chat/HelperBot/dispatch/topAgent/index.ts index ce9b4e1d9..2238743c5 100644 --- a/packages/service/core/chat/HelperBot/dispatch/topAgent/index.ts +++ b/packages/service/core/chat/HelperBot/dispatch/topAgent/index.ts @@ -1,4 +1,8 @@ -import type { HelperBotDispatchParamsType, HelperBotDispatchResponseType } from '../type'; +import { + AICollectionAnswerSchema, + type HelperBotDispatchParamsType, + type HelperBotDispatchResponseType +} from '../type'; import { helperChats2GPTMessages } from '@fastgpt/global/core/chat/helperBot/adaptor'; import { getPrompt } from './prompt'; import { createLLMResponse } from '../../../../ai/llm/request'; @@ -6,10 +10,17 @@ import { getLLMModel } from '../../../../ai/model'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; import { generateResourceList } from './utils'; -import { TopAgentFormDataSchema } from './type'; +import { TopAgentAnswerSchema, TopAgentFormDataSchema } from './type'; import { addLog } from '../../../../../common/system/log'; import { formatAIResponse } from '../utils'; import type { TopAgentParamsType } from '@fastgpt/global/core/chat/helperBot/topAgent/type'; +import type { + UserInputFormItemType, + UserInputInteractive +} from '@fastgpt/global/core/workflow/template/system/interactive/type'; +import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; export const dispatchTopAgent = async ( props: HelperBotDispatchParamsType @@ -37,27 +48,20 @@ export const dispatchTopAgent = async ( }); const historyMessages = helperChats2GPTMessages({ - messages: histories, - reserveTool: false + messages: histories }); const conversationMessages = [ { role: 'system' as const, content: systemPrompt }, ...historyMessages, { role: 'user' as const, content: query } ]; - + console.log(JSON.stringify(conversationMessages, null, 2)); const llmResponse = await createLLMResponse({ body: { messages: conversationMessages, model: modelData, stream: true }, - onStreaming: ({ text }) => { - workflowResponseWrite?.({ - event: SseResponseEventEnum.answer, - data: textAdaptGptResponse({ text }) - }); - }, onReasoning: ({ text }) => { workflowResponseWrite?.({ event: SseResponseEventEnum.answer, @@ -71,9 +75,9 @@ export const dispatchTopAgent = async ( const answerText = llmResponse.answerText; const reasoningText = llmResponse.reasoningText; - + console.log('Top agent response:', answerText); try { - const responseJson = JSON.parse(answerText); + const responseJson = TopAgentAnswerSchema.parse(JSON.parse(answerText)); if (responseJson.phase === 'generation') { addLog.debug('🔄 TopAgent: Configuration generation phase'); @@ -82,12 +86,12 @@ export const dispatchTopAgent = async ( role: responseJson.task_analysis?.role, taskObject: responseJson.task_analysis?.goal, tools: responseJson.resources?.tools?.map((tool: any) => tool.id), - fileUploadEnabled: responseJson.resources?.system_features?.file_upload?.enabled || false + fileUploadEnabled: responseJson.resources?.file_upload?.enabled }); if (formData) { workflowResponseWrite?.({ - event: SseResponseEventEnum.formData, + event: SseResponseEventEnum.topAgentConfig, data: formData }); } @@ -102,16 +106,60 @@ export const dispatchTopAgent = async ( } else if (responseJson.phase === 'collection') { addLog.debug('📝 TopAgent: Information collection phase'); - const displayText = responseJson.question || answerText; + const formDeata = responseJson.form; + if (formDeata) { + const inputForm: UserInputInteractive = { + type: 'userInput', + params: { + inputForm: formDeata.map((item) => { + return { + type: item.type as FlowNodeInputTypeEnum, + key: getNanoid(6), + label: item.label, + value: '', + required: false, + valueType: + item.type === FlowNodeInputTypeEnum.numberInput + ? WorkflowIOValueTypeEnum.number + : WorkflowIOValueTypeEnum.string, + list: + 'options' in item + ? item.options?.map((option) => ({ label: option, value: option })) + : undefined + }; + }), + description: responseJson.question + } + }; + workflowResponseWrite?.({ + event: SseResponseEventEnum.collectionForm, + data: inputForm + }); + + return { + aiResponse: formatAIResponse({ + text: responseJson.question, + reasoning: reasoningText, + collectionForm: inputForm + }), + usage + }; + } + + workflowResponseWrite?.({ + event: SseResponseEventEnum.answer, + data: textAdaptGptResponse({ text: responseJson.question }) + }); + return { aiResponse: formatAIResponse({ - text: displayText, - reasoning: responseJson.reasoning || reasoningText + text: responseJson.question, + reasoning: reasoningText }), usage }; } else { - addLog.warn(`[Top agent] Unknown phase: ${responseJson.phase}`); + addLog.warn(`[Top agent] Unknown phase`, responseJson); return { aiResponse: formatAIResponse({ text: answerText, diff --git a/packages/service/core/chat/HelperBot/dispatch/topAgent/prompt.ts b/packages/service/core/chat/HelperBot/dispatch/topAgent/prompt.ts index 053479fdf..a06b34d10 100644 --- a/packages/service/core/chat/HelperBot/dispatch/topAgent/prompt.ts +++ b/packages/service/core/chat/HelperBot/dispatch/topAgent/prompt.ts @@ -102,20 +102,50 @@ ${buildMetadataInfo(metadata)} "question": "实际向用户提出的问题内容" } -问题内容可以是开放式问题,也可以包含选项: +问题内容可以是开放式问题,也可以包含表单填写: -开放式问题示例: +开放式问题,无需表单填写: { "phase": "collection", "reasoning": "需要首先了解任务的基本定位和目标场景,这将决定后续需要确认的工具类型和能力边界", "question": "我想了解一下您希望这个流程模板实现什么功能?能否详细描述一下具体要处理什么样的任务或问题?" } -选择题示例: +表单示例,一共有 4 类表单类型: { "phase": "collection", "reasoning": "需要确认参数化设计的重点方向,这将影响流程模板的灵活性设计", - "question": "关于流程的参数化设计,用户最需要调整的是:\\nA. 输入数据源(不同类型的数据库/文件)\\nB. 处理参数(阈值、过滤条件、算法选择)\\nC. 输出格式(报告类型、文件格式、目标系统)\\nD. 执行环境(触发方式、频率、并发度)\\n\\n请选择最符合的选项,或输入您的详细回答:" + "question": "我需要和你确认一些参数,请根据你的需求选择对应的选项:", + "form": [ + { + "type": "input", + "label": "请输入你想优化的方向" + }, + { + "type": "numberInput", + "label": "你想优化多少次" + }, + { + "type": "select", + "label": "关于流程的参数化设计,用户最需要调整的是", + "options": [ + "输入数据源(不同类型的数据库/文件)", + "处理参数(阈值、过滤条件、算法选择)", + "输出格式(报告类型、文件格式、目标系统)", + "执行环境(触发方式、频率、并发度)" + ] + }, + { + "type": "multipleSelect", + "label": "你想了解用户什么信息", + "options": [ + "选项 A", + "选项 B", + "选项 C", + "选项 D" + ] + } + ] } 选项设计原则: @@ -277,24 +307,23 @@ ${resourceList} 直接输出以下格式的JSON(千万不要添加其他字段进来): { "phase": "generation", + "reasoning": "详细说明所有资源的选择理由:工具、知识库和系统功能如何协同工作来完成任务目标", "task_analysis": { "goal": "任务的核心目标描述", "role": "该流程的角色信息", "key_features": "收集到的信息,对任务的深度理解和定位" }, - "reasoning": "详细说明所有资源的选择理由:工具、知识库和系统功能如何协同工作来完成任务目标", "resources": { "tools": [ - {"id": "工具ID", "type": "tool"} + "工具ID" ], "knowledges": [ - {"id": "知识库ID", "type": "knowledge"} + "知识库ID" ], "system_features": { "file_upload": { "enabled": true/false, - "purpose": "说明原因(enabled=true时必填)", - "file_types": ["可选的文件类型"] + "purpose": "说明原因(enabled=true时必填)" } } } @@ -309,7 +338,6 @@ ${resourceList} * system_features: 系统功能配置对象 - file_upload.enabled: 是否需要文件上传(必填) - file_upload.purpose: 为什么需要(enabled=true时必填) - - file_upload.file_types: 建议的文件类型(可选),如["pdf", "xlsx"] **✅ 正确示例1**(需要文件上传): { @@ -321,14 +349,13 @@ ${resourceList} "reasoning": "使用数据分析工具处理Excel数据,需要用户上传自己的财务报表文件", "resources": { "tools": [ - {"id": "data_analysis/tool", "type": "tool"} + "data_analysis/tool" ], "knowledges": [], "system_features": { "file_upload": { "enabled": true, - "purpose": "需要您上传财务报表文件(Excel或PDF格式)进行数据提取和分析", - "file_types": ["xlsx", "xls", "pdf"] + "purpose": "需要您上传财务报表文件(Excel或PDF格式)进行数据提取和分析" } } } @@ -340,10 +367,10 @@ ${resourceList} "reasoning": "使用搜索工具获取实时信息,结合知识库的专业知识", "resources": { "tools": [ - {"id": "metaso/metasoSearch", "type": "tool"} + "metaso/metasoSearch" ], "knowledges": [ - {"id": "travel_kb", "type": "knowledge"} + "travel_kb" ], "system_features": { "file_upload": { @@ -374,9 +401,9 @@ ${resourceList} { "resources": { "tools": [ - {"id": "bing/webSearch", "type": "tool"}, - {"id": "google/search", "type": "tool"}, - {"id": "metaso/metasoSearch", "type": "tool"} + "bing/webSearch", + "google/search", + "metaso/metasoSearch" // ❌ 错误:这三个都是网页搜索工具,只应该选择一个最合适的 ] } @@ -437,7 +464,7 @@ ${resourceList} **回复格式要求**: - **所有回复必须是 JSON 格式**,包含 \`phase\` 字段 -- 信息收集阶段:输出 \`{"phase": "collection", "reasoning": "...", "question": "..."}\` +- 信息收集阶段:输出 \`{"phase": "collection", "reasoning": "...", "question": "...","form":[...]}\` - 配置生成阶段:输出 \`{"phase": "generation", "task_analysis": {...}, "resources": {...}, ...}\` - ❌ 不要输出任何非 JSON 格式的内容 - ❌ 不要添加代码块标记(如 \\\`\\\`\\\`json) diff --git a/packages/service/core/chat/HelperBot/dispatch/topAgent/type.ts b/packages/service/core/chat/HelperBot/dispatch/topAgent/type.ts index d1f72af57..5dae7a098 100644 --- a/packages/service/core/chat/HelperBot/dispatch/topAgent/type.ts +++ b/packages/service/core/chat/HelperBot/dispatch/topAgent/type.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { AICollectionAnswerSchema } from '../type'; export const TopAgentFormDataSchema = z.object({ role: z.string().optional(), @@ -7,3 +8,31 @@ export const TopAgentFormDataSchema = z.object({ fileUploadEnabled: z.boolean().optional().default(false) }); export type TopAgentFormDataType = z.infer; + +// 表单收集 +export const TopAgentCollectionAnswerSchema = AICollectionAnswerSchema.extend({ + phase: z.literal('collection'), + reasoning: z.string().nullish() +}); +export const TopAgentGenerationAnswerSchema = z.object({ + phase: z.literal('generation'), + reasoning: z.string().nullish(), + task_analysis: z.object({ + goal: z.string(), + role: z.string(), + key_features: z.string() + }), + resources: z.object({ + tools: z.array(z.string()), + knowledges: z.array(z.string()), + file_upload: z.object({ + enabled: z.boolean(), + purpose: z.string() + }) + }) +}); +export const TopAgentAnswerSchema = z.discriminatedUnion('phase', [ + TopAgentCollectionAnswerSchema, + TopAgentGenerationAnswerSchema +]); +export type TopAgentAnswerType = z.infer; diff --git a/packages/service/core/chat/HelperBot/dispatch/type.ts b/packages/service/core/chat/HelperBot/dispatch/type.ts index edfbe6754..f5581562f 100644 --- a/packages/service/core/chat/HelperBot/dispatch/type.ts +++ b/packages/service/core/chat/HelperBot/dispatch/type.ts @@ -6,6 +6,7 @@ import { } from '@fastgpt/global/core/chat/helperBot/type'; import { WorkflowResponseFnSchema } from '../../../workflow/dispatch/type'; import { LocaleList } from '@fastgpt/global/common/i18n/type'; +import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; export const HelperBotDispatchParamsSchema = z.object({ query: z.string(), @@ -40,3 +41,19 @@ export const HelperBotDispatchResponseSchema = z.object({ }) }); export type HelperBotDispatchResponseType = z.infer; + +/* AI 表单输出 schema */ +const InputSchema = z.object({ + type: z.enum([FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.numberInput]), + label: z.string() +}); +const SelectSchema = z.object({ + type: z.enum([FlowNodeInputTypeEnum.select, FlowNodeInputTypeEnum.multipleSelect]), + label: z.string(), + options: z.array(z.string()) +}); +export const AICollectionAnswerSchema = z.object({ + question: z.string(), // 可能只有一个问题,可能 + form: z.array(z.union([InputSchema, SelectSchema])).optional() +}); +export type AICollectionAnswerType = z.infer; diff --git a/packages/service/core/chat/HelperBot/dispatch/utils.ts b/packages/service/core/chat/HelperBot/dispatch/utils.ts index 9dff148a6..250aa9511 100644 --- a/packages/service/core/chat/HelperBot/dispatch/utils.ts +++ b/packages/service/core/chat/HelperBot/dispatch/utils.ts @@ -1,11 +1,14 @@ import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/helperBot/type'; +import type { UserInputInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type'; export const formatAIResponse = ({ text, - reasoning + reasoning, + collectionForm }: { text: string; reasoning?: string; + collectionForm?: UserInputInteractive; }): AIChatItemValueItemType[] => { const result: AIChatItemValueItemType[] = []; @@ -23,5 +26,11 @@ export const formatAIResponse = ({ } }); + if (collectionForm) { + result.push({ + collectionForm + }); + } + return result; }; diff --git a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx index 608829aa9..7bc61476e 100644 --- a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx @@ -38,7 +38,6 @@ import { MaxLengthPlugin } from './plugins/MaxLengthPlugin'; import { VariableLabelNode } from './plugins/VariableLabelPlugin/node'; import VariableLabelPlugin from './plugins/VariableLabelPlugin'; import { useDeepCompareEffect } from 'ahooks'; -import VariablePickerPlugin from './plugins/VariablePickerPlugin'; import MarkdownPlugin from './plugins/MarkdownPlugin'; import MyIcon from '../../Icon'; import ListExitPlugin from './plugins/ListExitPlugin'; @@ -48,7 +47,6 @@ import type { SkillLabelItemType } from './plugins/SkillLabelPlugin'; import SkillLabelPlugin from './plugins/SkillLabelPlugin'; import { SkillNode } from './plugins/SkillLabelPlugin/node'; import type { SkillOptionItemType } from './plugins/SkillPickerPlugin'; -import { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node'; const Placeholder = ({ children, padding }: { children: React.ReactNode; padding: string }) => ( { + const item = skillOptions[currentColumnIndex]?.list[currentRowIndex]; + if (!item || !item.canClick) return null; + return item; + }, [skillOptions, currentColumnIndex, currentRowIndex]); + // Recursively render item list const renderItemList = useCallback( ( @@ -661,8 +675,8 @@ export default function SkillPickerPlugin({ ) : columnData.onFolderLoad ? ( ) : null} - {item.icon && } + {item.icon && } {/* Folder content */} {item.label} @@ -672,9 +686,6 @@ export default function SkillPickerPlugin({ )} - {item.showArrow && ( - - )} ); @@ -779,8 +790,12 @@ export default function SkillPickerPlugin({ // Insert skill node text at current selection selection.insertNodes([$createTextNode(`{{@${skillId}@}}`)]); - closeMenu(); }); + + // Close menu after editor update to avoid flushSync warning + setTimeout(() => { + closeMenu(); + }, 0); } else { // If onClick didn't return a skillId, just close the menu closeMenu(); @@ -821,6 +836,63 @@ export default function SkillPickerPlugin({ {skillOptions.map((column, index) => { return renderColumn(column, index); })} + + {selectedTool && ( + + + {selectedTool.icon && ( + + )} + {/* Folder content */} + + {selectedTool.label} + + + + {selectedTool.description || t('app:tool_not_desc')} + + {/* Tools */} + {selectedTool.tools && selectedTool.tools.length > 0 && ( + <> + + {t('app:tools')}({selectedTool.tools.length}) + + {selectedTool.tools.map((tool) => ( + + + {tool.name} + + ))} + + )} + + )} , anchorElementRef.current! ); diff --git a/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPickerPlugin/index.tsx b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPickerPlugin/index.tsx index cbe01d4f5..1e23dd9cd 100644 --- a/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPickerPlugin/index.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPickerPlugin/index.tsx @@ -57,8 +57,12 @@ export default function VariableLabelPickerPlugin({ selection.insertNodes([ $createTextNode(`{{$${selectedOption.parent?.id}.${selectedOption.key}$}}`) ]); - closeMenu(); }); + + // Close menu after editor update to avoid flushSync warning + setTimeout(() => { + closeMenu(); + }, 0); }, [editor] ); diff --git a/packages/web/components/common/Textarea/PromptEditor/plugins/VariablePickerPlugin/index.tsx b/packages/web/components/common/Textarea/PromptEditor/plugins/VariablePickerPlugin/index.tsx index 49c95675c..c734dec80 100644 --- a/packages/web/components/common/Textarea/PromptEditor/plugins/VariablePickerPlugin/index.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/plugins/VariablePickerPlugin/index.tsx @@ -35,8 +35,12 @@ export default function VariablePickerPlugin({ nodeToRemove.remove(); } selection.insertNodes([$createTextNode(`{{${selectedOption.key}}}`)]); - closeMenu(); }); + + // Close menu after editor update to avoid flushSync warning + setTimeout(() => { + closeMenu(); + }, 0); }, [editor] ); diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 51aa4e0e7..fef2b446c 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -58,7 +58,6 @@ "auto_execute_default_prompt_placeholder": "Default questions sent when executing automatically", "auto_execute_tip": "After turning it on, the workflow will be automatically triggered when the user enters the conversation interface. \nExecution order: 1. Dialogue starter; 2. Global variables; 3. Automatic execution.", "auto_save": "Auto save", - "can_select_toolset": "Entire toolset available for selection", "change_app_type": "Change App Type", "chat_agent_intro": "AI autonomously plans executable processes", "chat_debug": "Chat Preview", @@ -392,6 +391,7 @@ "tool_detail": "Tool details", "tool_input_param_tip": "This tool requires configuration of relevant information for normal operation.", "tool_not_active": "This tool has not been activated yet", + "tool_not_desc": "The tool lacks a description ~", "tool_offset_tips": "This tool is no longer available and will interrupt application operation. Please replace it immediately.", "tool_param_config": "Parameter configuration", "tool_params_description_tips": "The description of parameter functions, if used as tool invocation parameters, affects the model tool invocation effect.", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index 7712fc1c1..25d907e33 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -60,7 +60,6 @@ "auto_execute_default_prompt_placeholder": "自动执行时,发送的默认问题", "auto_execute_tip": "开启后,用户进入对话界面将自动触发工作流。执行顺序:1、对话开场白;2、全局变量;3、自动执行。", "auto_save": "自动保存", - "can_select_toolset": "可选择整个工具集", "change_app_type": "更改应用类型", "chat_agent_intro": "由 AI 自主规划可执行流程", "chat_debug": "调试预览", @@ -411,6 +410,7 @@ "tool_input_param_tip": "该工具正常运行需要配置相关信息", "tool_load_failed": "部分工具加载失败", "tool_not_active": "该工具尚未激活", + "tool_not_desc": "工具缺少描述~", "tool_offset_tips": "该工具已无法使用,将中断应用运行,请立即替换", "tool_param_config": "参数配置", "tool_params_description_tips": "参数功能的描述,若作为工具调用参数,影响模型工具调用效果", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 99d3493fc..3f0d81523 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -58,7 +58,6 @@ "auto_execute_default_prompt_placeholder": "自動執行時,傳送的預設問題", "auto_execute_tip": "開啟後,使用者進入對話式介面將自動觸發工作流程。\n執行順序:1、對話開場白;2、全域變數;3、自動執行。", "auto_save": "自動儲存", - "can_select_toolset": "可選擇整個工具集", "change_app_type": "更改應用程式類型", "chat_agent_intro": "由 AI 自主規劃可執行流程", "chat_debug": "聊天預覽", @@ -388,6 +387,7 @@ "tool_detail": "工具詳情", "tool_input_param_tip": "該工具正常運行需要配置相關信息", "tool_not_active": "該工具尚未激活", + "tool_not_desc": "工具缺少描述~", "tool_offset_tips": "該工具已無法使用,將中斷應用運行,請立即替換", "tool_param_config": "參數配置", "tool_params_description_tips": "參數功能的描述,若作為工具調用參數,影響模型工具調用效果", diff --git a/projects/app/src/components/core/chat/ChatContainer/type.d.ts b/projects/app/src/components/core/chat/ChatContainer/type.d.ts index 93dd2395b..521924609 100644 --- a/projects/app/src/components/core/chat/ChatContainer/type.d.ts +++ b/projects/app/src/components/core/chat/ChatContainer/type.d.ts @@ -6,7 +6,10 @@ import type { type ToolModuleResponseItemType } from '@fastgpt/global/core/chat/type'; import type { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; -import type { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type'; +import type { + UserInputInteractive, + WorkflowInteractiveResponseType +} from '@fastgpt/global/core/workflow/template/system/interactive/type'; import type { TopAgentFormDataType } from '@fastgpt/service/core/chat/HelperBot/dispatch/topAgent/type'; import type { GeneratedSkillDataType } from '@fastgpt/global/core/chat/helperBot/generatedSkill/type'; @@ -26,7 +29,8 @@ export type generatingMessageProps = { nodeResponse?: ChatHistoryItemResType; durationSeconds?: number; - // Agent + // HelperBot + collectionForm?: UserInputInteractive; formData?: TopAgentFormDataType; generatedSkill?: GeneratedSkillDataType; }; diff --git a/projects/app/src/components/core/chat/HelperBot/components/AIItem.tsx b/projects/app/src/components/core/chat/HelperBot/components/AIItem.tsx index 7efd0556e..76d4663d2 100644 --- a/projects/app/src/components/core/chat/HelperBot/components/AIItem.tsx +++ b/projects/app/src/components/core/chat/HelperBot/components/AIItem.tsx @@ -8,7 +8,8 @@ import { AccordionItem, AccordionPanel, Flex, - HStack + HStack, + Button } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -16,6 +17,11 @@ import Markdown from '@/components/Markdown'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyIconButton from '@fastgpt/web/components/common/Icon/button'; import { useCopyData } from '@fastgpt/web/hooks/useCopyData'; +import type { UserInputInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type'; +import { Controller, useForm } from 'react-hook-form'; +import { nodeInputTypeToInputType } from '@/components/core/app/formRender/utils'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import InputRender from '@/components/core/app/formRender'; const accordionButtonStyle = { w: 'auto', @@ -69,7 +75,6 @@ const RenderResoningContent = React.memo(function RenderResoningContent({ ); }); - const RenderText = React.memo(function RenderText({ showAnimation, text @@ -86,19 +91,87 @@ const RenderText = React.memo(function RenderText({ return ; }); +const RenderCollectionForm = React.memo(function RenderCollectionForm({ + collectionForm, + onSubmit +}: { + collectionForm: UserInputInteractive; + onSubmit: (formData: string) => void; +}) { + const { t } = useTranslation(); + const { control, handleSubmit } = useForm(); + + const submitted = collectionForm.params.submitted; + + return ( + + {collectionForm.params.description} + + {collectionForm.params.inputForm.map((input) => { + const inputType = nodeInputTypeToInputType([input.type]); + + return ( + { + return ( + + + {input.label} + + + + ); + }} + /> + ); + })} + + + {!submitted && ( + + + + )} + + ); +}); const AIItem = ({ chat, isChatting, - isLastChild + isLastChild, + onSubmitCollectionForm }: { chat: HelperBotChatItemSiteType; isChatting: boolean; isLastChild: boolean; + onSubmitCollectionForm: (formData: string) => void; }) => { const { t } = useTranslation(); const { copyData } = useCopyData(); - + console.log(chat, 111122); return ( ); } + if ('collectionForm' in value && value.collectionForm) { + return ( + + ); + } })} {/* Controller */} diff --git a/projects/app/src/components/core/chat/HelperBot/index.tsx b/projects/app/src/components/core/chat/HelperBot/index.tsx index aa5ed323d..ce34cfe0d 100644 --- a/projects/app/src/components/core/chat/HelperBot/index.tsx +++ b/projects/app/src/components/core/chat/HelperBot/index.tsx @@ -1,7 +1,7 @@ -import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; +import React, { useCallback, useImperativeHandle, useRef, useState } from 'react'; -import HelperBotContextProvider, { type HelperBotRefType, type HelperBotProps } from './context'; -import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type'; +import HelperBotContextProvider, { type HelperBotProps } from './context'; +import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/helperBot/type'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import MyBox from '@fastgpt/web/components/common/MyBox'; import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; @@ -145,7 +145,7 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro event, text = '', reasoningText, - tool, + collectionForm, formData, generatedSkill }: generatingMessageProps) => { @@ -158,26 +158,34 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro const updateValue: AIChatItemValueItemType = item.value[updateIndex]; // Special event: form data - if (event === SseResponseEventEnum.formData && formData) { - if (type === HelperBotTypeEnum.topAgent) { - onApply?.(formData); - } + if (event === SseResponseEventEnum.collectionForm && collectionForm) { + return { + ...item, + value: item.value.concat({ + collectionForm + }) + }; + } + if ( + event === SseResponseEventEnum.topAgentConfig && + formData && + type === HelperBotTypeEnum.topAgent + ) { + onApply(formData); return item; } - - // Special event: generated skill - if (event === SseResponseEventEnum.generatedSkill && generatedSkill) { - console.log('📊 HelperBot: Received generatedSkill event', generatedSkill); - // 直接将生成的 skill 数据传递给 onApply 回调(仅在 skillAgent 类型时) - if (type === HelperBotTypeEnum.skillAgent) { - onApply?.(generatedSkill); - } + if ( + event === SseResponseEventEnum.generatedSkill && + generatedSkill && + type === HelperBotTypeEnum.skillAgent + ) { + onApply(generatedSkill); return item; } if (event === SseResponseEventEnum.answer || event === SseResponseEventEnum.fastAnswer) { if (reasoningText) { - if (updateValue?.reasoning) { + if ('reasoning' in updateValue && updateValue.reasoning) { updateValue.reasoning.content += reasoningText; return { ...item, @@ -200,7 +208,7 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro } } if (text) { - if (updateValue?.text) { + if ('text' in updateValue && updateValue.text) { updateValue.text.content += text; return { ...item, @@ -224,50 +232,6 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro } } - // Tool call - if (event === SseResponseEventEnum.toolCall && tool) { - const val: AIChatItemValueItemType = { - tool: { - ...tool, - response: '' - } - }; - return { - ...item, - value: [...item.value, val] - }; - } - if (event === SseResponseEventEnum.toolParams && tool && updateValue?.tool) { - if (tool.params) { - updateValue.tool.params += tool.params; - return { - ...item, - value: [ - ...item.value.slice(0, updateIndex), - updateValue, - ...item.value.slice(updateIndex + 1) - ] - }; - } - return item; - } - if (event === SseResponseEventEnum.toolResponse && tool && updateValue?.tool) { - if (tool.response) { - // replace tool response - updateValue.tool.response += tool.response; - - return { - ...item, - value: [ - ...item.value.slice(0, updateIndex), - updateValue, - ...item.value.slice(updateIndex + 1) - ] - }; - } - return item; - } - return item; }) ); @@ -275,91 +239,98 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro generatingScroll(false); } ); - const handleSendMessage = useMemoizedFn(async ({ query = '' }: onSendMessageParamsType) => { - // Init check - if (isChatting) { - return toast({ - title: t('chat:is_chatting'), - status: 'warning' - }); - } - - abortRequest(); - query = query.trim(); - - if (!query) { - toast({ - title: t('chat:content_empty'), - status: 'warning' - }); - return; - } - - const chatItemDataId = getNanoid(24); - const newChatList: HelperBotChatItemSiteType[] = [ - ...chatRecords, - // 用户消息 - { - _id: getNanoid(24), - createTime: new Date(), - dataId: chatItemDataId, - obj: ChatRoleEnum.Human, - value: [ - { - text: { - content: query - } - } - ] - }, - // AI 消息 - 空白,用于接收流式输出 - { - _id: getNanoid(24), - createTime: new Date(), - dataId: chatItemDataId, - obj: ChatRoleEnum.AI, - value: [ - { - text: { - content: '' - } - } - ] + const handleSendMessage = useMemoizedFn( + async ({ query = '', collectionFormData }: onSendMessageParamsType) => { + // Init check + if (isChatting) { + return toast({ + title: t('chat:is_chatting'), + status: 'warning' + }); } - ]; - setChatRecords(newChatList); - resetInputVal({}); - scrollToBottom(); + abortRequest(); + query = query.trim(); + const mergeQuery = query || collectionFormData; - setIsChatting(true); - try { - const abortSignal = new AbortController(); - chatController.current = abortSignal; - console.log('metadata-fronted', metadata); - const { responseText } = await streamFetch({ - url: '/api/core/chat/helperBot/completions', - data: { - chatId, - chatItemId: chatItemDataId, - query, - files: chatForm.getValues('files').map((item) => ({ - type: item.type, - key: item.key, - // url: item.url, - name: item.name - })), - metadata: { - type: type, - data: metadata - } + if (!mergeQuery) { + toast({ + title: t('chat:content_empty'), + status: 'warning' + }); + return; + } + + const chatItemDataId = getNanoid(24); + const newChatList: HelperBotChatItemSiteType[] = [ + ...chatRecords, + // 用户消息 + { + _id: getNanoid(24), + createTime: new Date(), + dataId: chatItemDataId, + obj: ChatRoleEnum.Human, + value: [ + { + text: { + content: mergeQuery + } + } + ] }, - onMessage: generatingMessage, - abortCtrl: abortSignal - }); - } catch (error) {} - setIsChatting(false); - }); + // AI 消息 - 空白,用于接收流式输出 + ...(query + ? [ + { + _id: getNanoid(24), + createTime: new Date(), + dataId: chatItemDataId, + obj: ChatRoleEnum.AI, + value: [ + { + text: { + content: '' + } + } + ] + } + ] + : []) + ]; + setChatRecords(newChatList); + + resetInputVal({}); + scrollToBottom(); + + setIsChatting(true); + try { + const abortSignal = new AbortController(); + chatController.current = abortSignal; + + const { responseText } = await streamFetch({ + url: '/api/core/chat/helperBot/completions', + data: { + chatId, + chatItemId: chatItemDataId, + query: mergeQuery, + files: chatForm.getValues('files').map((item) => ({ + type: item.type, + key: item.key, + // url: item.url, + name: item.name + })), + metadata: { + type: type, + data: metadata + } + }, + onMessage: generatingMessage, + abortCtrl: abortSignal + }); + } catch (error) {} + setIsChatting(false); + } + ); useImperativeHandle(ChatBoxRef, () => ({ restartChat() { @@ -397,6 +368,7 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro chat={item} isChatting={isChatting} isLastChild={index === chatRecords.length - 1} + onSubmitCollectionForm={(data) => handleSendMessage({ query: data })} /> )} diff --git a/projects/app/src/components/core/chat/HelperBot/type.d.ts b/projects/app/src/components/core/chat/HelperBot/type.d.ts index 9c7b7ee81..e6c0be09e 100644 --- a/projects/app/src/components/core/chat/HelperBot/type.d.ts +++ b/projects/app/src/components/core/chat/HelperBot/type.d.ts @@ -2,6 +2,7 @@ import type { UserInputFileItemType } from '../ChatContainer/ChatBox/type'; export type onSendMessageParamsType = { query?: string; + collectionFormData?: string; files?: UserInputFileItemType[]; }; export type onSendMessageFnType = (e: onSendMessageParamsType) => Promise; diff --git a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/EditForm.tsx b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/EditForm.tsx index a6c556f8b..6b48eb92e 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/EditForm.tsx +++ b/projects/app/src/pageComponents/app/detail/Edit/ChatAgent/SkillEdit/EditForm.tsx @@ -3,7 +3,6 @@ import { Box, Flex, Grid, - useTheme, useDisclosure, Button, HStack, @@ -16,10 +15,9 @@ import { type AppFileSelectConfigType } from '@fastgpt/global/core/app/type/conf import type { SelectedToolItemType, SkillEditType } from '@fastgpt/global/core/app/formEdit/type'; import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; -import { useForm } from 'react-hook-form'; +import { useFieldArray, useForm } from 'react-hook-form'; import dynamic from 'next/dynamic'; -import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import Avatar from '@fastgpt/web/components/common/Avatar'; import MyIcon from '@fastgpt/web/components/common/Icon'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; @@ -32,6 +30,8 @@ import { useContextSelector } from 'use-context-selector'; import { AppContext } from '@/pageComponents/app/detail/context'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useSkillManager } from '../hooks/useSkillManager'; +import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal')); @@ -52,7 +52,6 @@ const EditForm = ({ onClose, onSave }: EditFormProps) => { - const theme = useTheme(); const router = useRouter(); const { t } = useTranslation(); const appId = useContextSelector(AppContext, (v) => v.appId); @@ -64,7 +63,8 @@ const EditForm = ({ watch, setValue, reset, - formState: { isDirty } + formState: { isDirty }, + control } = useForm({ defaultValues: skill }); @@ -75,10 +75,39 @@ const EditForm = ({ }, [skill, reset]); const selectedModel = getWebLLMModel(model); - const selectedTools = watch('selectedTools') || []; + const stepsText = watch('stepsText') || ''; const selectDatasets = watch('dataset.list') || []; const skillName = watch('name'); + const { + fields: selectedTools, + prepend: prependSelectedTools, + remove: removeSelectedTools, + update: updateSelectedTools + } = useFieldArray({ + control, + name: 'selectedTools', + keyName: '_id' + }); + + const { skillOption, selectedSkills, onClickSkill, onRemoveSkill, SkillModal } = useSkillManager({ + topAgentSelectedTools, + selectedTools, + onDeleteTool: (id) => { + removeSelectedTools(selectedTools.findIndex((item) => item.id === id)); + }, + onUpdateOrAddTool: (tool) => { + const index = selectedTools.findIndex((item) => item.id === tool.id); + if (index === -1) { + prependSelectedTools(tool); + } else { + updateSelectedTools(index, tool); + } + }, + canSelectFile: fileSelectConfig?.canSelectFile, + canSelectImg: fileSelectConfig?.canSelectImg + }); + const { isOpen: isOpenDatasetSelect, onOpen: onOpenKbSelect, @@ -204,7 +233,21 @@ const EditForm = ({ {t('app:execution_steps')} -