mirror of
https://github.com/labring/FastGPT.git
synced 2025-12-25 20:02:47 +00:00
perf: helperbot ui
This commit is contained in:
parent
1f100276f6
commit
363f9b888a
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<typeof AIChatItemValueItemSchema>;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<TopAgentParamsType>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
|||
<conversation_rules>
|
||||
**回复格式要求**:
|
||||
- **所有回复必须是 JSON 格式**,包含 \`phase\` 字段
|
||||
- 信息收集阶段:输出 \`{"phase": "collection", "reasoning": "...", "question": "..."}\`
|
||||
- 信息收集阶段:输出 \`{"phase": "collection", "reasoning": "...", "question": "...","form":[...]}\`
|
||||
- 配置生成阶段:输出 \`{"phase": "generation", "task_analysis": {...}, "resources": {...}, ...}\`
|
||||
- ❌ 不要输出任何非 JSON 格式的内容
|
||||
- ❌ 不要添加代码块标记(如 \\\`\\\`\\\`json)
|
||||
|
|
|
|||
|
|
@ -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<typeof TopAgentFormDataSchema>;
|
||||
|
||||
// 表单收集
|
||||
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<typeof TopAgentAnswerSchema>;
|
||||
|
|
|
|||
|
|
@ -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<typeof HelperBotDispatchResponseSchema>;
|
||||
|
||||
/* 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<typeof AICollectionAnswerSchema>;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
<Box
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import { mergeRegister } from '@lexical/utils';
|
|||
import { registerLexicalTextEntity } from '../../utils';
|
||||
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type';
|
||||
|
||||
const REGEX = new RegExp(getSkillRegexString(), 'i');
|
||||
|
||||
export type SkillLabelItemType = FlowNodeTemplateType & {
|
||||
configStatus: 'active' | 'invalid' | 'waitingForConfig';
|
||||
export type SkillLabelItemType = SelectedToolItemType & {
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
import React, { useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { Box, Flex, HStack } from '@chakra-ui/react';
|
||||
import { useBasicTypeaheadTriggerMatch } from '../../utils';
|
||||
import Avatar from '../../../../Avatar';
|
||||
import MyIcon from '../../../../Icon';
|
||||
|
|
@ -27,6 +27,10 @@ import { useRequest2 } from '../../../../../../hooks/useRequest';
|
|||
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
/*
|
||||
两列渲染器
|
||||
第三列会展示第二列选中的 tool/toolset 的详细信息:头像、名称、描述、子工具(如有)
|
||||
*/
|
||||
export type SkillOptionItemType = {
|
||||
description?: string;
|
||||
list: SkillItemType[];
|
||||
|
|
@ -40,16 +44,20 @@ export type SkillItemType = {
|
|||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
canClick: boolean;
|
||||
children?: SkillOptionItemType;
|
||||
|
||||
// Folder
|
||||
open?: boolean;
|
||||
isFolder?: boolean;
|
||||
folderChildren?: SkillItemType[];
|
||||
|
||||
// System tool/ model
|
||||
showArrow?: boolean;
|
||||
children?: SkillOptionItemType;
|
||||
// Toolset
|
||||
tools?: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export default function SkillPickerPlugin({
|
||||
|
|
@ -558,6 +566,12 @@ export default function SkillPickerPlugin({
|
|||
getFlattenedVisibleItems
|
||||
]);
|
||||
|
||||
const selectedTool = useMemo(() => {
|
||||
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 ? (
|
||||
<Box w={3} flexShrink={0} />
|
||||
) : null}
|
||||
{item.icon && <Avatar src={item.icon} w={'1.2rem'} borderRadius={'xs'} />}
|
||||
|
||||
{item.icon && <Avatar src={item.icon} w={'1.2rem'} borderRadius={'xs'} />}
|
||||
{/* Folder content */}
|
||||
<Box fontSize={'sm'} fontWeight={'medium'} flex={'1 0 0'} className="textEllipsis">
|
||||
{item.label}
|
||||
|
|
@ -672,9 +686,6 @@ export default function SkillPickerPlugin({
|
|||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{item.showArrow && (
|
||||
<MyIcon name={'core/chat/chevronRight'} w={'0.8rem'} color={'myGray.400'} />
|
||||
)}
|
||||
</MyBox>
|
||||
);
|
||||
|
||||
|
|
@ -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 && (
|
||||
<Box
|
||||
ml={2}
|
||||
p={2.5}
|
||||
borderRadius={'sm'}
|
||||
w={'200px'}
|
||||
boxShadow={
|
||||
'0 4px 10px 0 rgba(19, 51, 107, 0.10), 0 0 1px 0 rgba(19, 51, 107, 0.10)'
|
||||
}
|
||||
bg={'white'}
|
||||
flexShrink={0}
|
||||
maxH={'350px'}
|
||||
overflow={'auto'}
|
||||
>
|
||||
<HStack>
|
||||
{selectedTool.icon && (
|
||||
<Avatar src={selectedTool.icon} w={'1.3rem'} borderRadius={'xs'} />
|
||||
)}
|
||||
{/* Folder content */}
|
||||
<Box fontSize={'sm'} fontWeight={'medium'}>
|
||||
{selectedTool.label}
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box color={'myGray.500'} fontSize={'xs'} className="textEllipsis3">
|
||||
{selectedTool.description || t('app:tool_not_desc')}
|
||||
</Box>
|
||||
{/* Tools */}
|
||||
{selectedTool.tools && selectedTool.tools.length > 0 && (
|
||||
<>
|
||||
<Box mt={2} color={'myGray.900'} fontSize={'sm'}>
|
||||
{t('app:tools')}({selectedTool.tools.length})
|
||||
</Box>
|
||||
{selectedTool.tools.map((tool) => (
|
||||
<Box
|
||||
key={tool.id}
|
||||
mt={1}
|
||||
fontSize={'xs'}
|
||||
color={'myGray.600'}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
gap={1.5}
|
||||
>
|
||||
<Box
|
||||
w={'6px'}
|
||||
h={'6px'}
|
||||
borderRadius={'50%'}
|
||||
bg={'primary.600'}
|
||||
flexShrink={0}
|
||||
/>
|
||||
{tool.name}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>,
|
||||
anchorElementRef.current!
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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": "参数功能的描述,若作为工具调用参数,影响模型工具调用效果",
|
||||
|
|
|
|||
|
|
@ -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": "參數功能的描述,若作為工具調用參數,影響模型工具調用效果",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</Accordion>
|
||||
);
|
||||
});
|
||||
|
||||
const RenderText = React.memo(function RenderText({
|
||||
showAnimation,
|
||||
text
|
||||
|
|
@ -86,19 +91,87 @@ const RenderText = React.memo(function RenderText({
|
|||
|
||||
return <Markdown source={source} showAnimation={showAnimation} />;
|
||||
});
|
||||
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 (
|
||||
<Box>
|
||||
<Box mb={3}>{collectionForm.params.description}</Box>
|
||||
<Flex flexDirection={'column'} gap={3}>
|
||||
{collectionForm.params.inputForm.map((input) => {
|
||||
const inputType = nodeInputTypeToInputType([input.type]);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
key={input.key}
|
||||
control={control}
|
||||
name={input.key}
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||
return (
|
||||
<Box>
|
||||
<FormLabel whiteSpace={'pre-wrap'} mb={0.5}>
|
||||
{input.label}
|
||||
</FormLabel>
|
||||
<InputRender
|
||||
{...input}
|
||||
inputType={inputType}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isDisabled={submitted}
|
||||
isInvalid={!!error}
|
||||
isRichText={false}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
|
||||
{!submitted && (
|
||||
<Flex justifyContent={'flex-end'} mt={4}>
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={handleSubmit((data) => {
|
||||
// 需要把 label 作为 key
|
||||
const dataByLabel = Object.fromEntries(
|
||||
collectionForm.params.inputForm.map((input) => [input.label, data[input.key]])
|
||||
);
|
||||
onSubmit(JSON.stringify(dataByLabel));
|
||||
})}
|
||||
>
|
||||
{t('common:Submit')}
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<Box
|
||||
_hover={{
|
||||
|
|
@ -136,6 +209,15 @@ const AIItem = ({
|
|||
/>
|
||||
);
|
||||
}
|
||||
if ('collectionForm' in value && value.collectionForm) {
|
||||
return (
|
||||
<RenderCollectionForm
|
||||
key={i}
|
||||
collectionForm={value.collectionForm}
|
||||
onSubmit={onSubmitCollectionForm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
{/* Controller */}
|
||||
|
|
|
|||
|
|
@ -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 })}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -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<any>;
|
||||
|
|
|
|||
|
|
@ -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<SkillEditType>({
|
||||
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 = ({
|
|||
<FormLabel>{t('app:execution_steps')}</FormLabel>
|
||||
</HStack>
|
||||
<Box mt={2}>
|
||||
<Textarea
|
||||
<PromptEditor
|
||||
minH={160}
|
||||
title={t('app:execution_steps')}
|
||||
placeholder={t('app:no_steps_yet')}
|
||||
isRichText
|
||||
skillOption={skillOption}
|
||||
selectedSkills={selectedSkills}
|
||||
onClickSkill={onClickSkill}
|
||||
onRemoveSkill={onRemoveSkill}
|
||||
value={stepsText}
|
||||
onChange={(e) => {
|
||||
setValue('stepsText', e);
|
||||
}}
|
||||
/>
|
||||
{/* <Textarea
|
||||
{...register('stepsText')}
|
||||
maxLength={1000000}
|
||||
bg={'myGray.50'}
|
||||
|
|
@ -213,7 +256,7 @@ const EditForm = ({
|
|||
placeholder={t('app:no_steps_yet')}
|
||||
fontSize={'sm'}
|
||||
color={'myGray.900'}
|
||||
/>
|
||||
/> */}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
|
@ -225,23 +268,16 @@ const EditForm = ({
|
|||
selectedTools={selectedTools}
|
||||
fileSelectConfig={fileSelectConfig}
|
||||
onAddTool={(e) => {
|
||||
setValue('selectedTools', [e, ...(selectedTools || [])], { shouldDirty: true });
|
||||
prependSelectedTools(e);
|
||||
}}
|
||||
onUpdateTool={(e) => {
|
||||
setValue(
|
||||
'selectedTools',
|
||||
selectedTools?.map((item) => (item.id === e.id ? e : item)) || [],
|
||||
{ shouldDirty: true }
|
||||
updateSelectedTools(
|
||||
selectedTools.findIndex((item) => item.id === e.id),
|
||||
e
|
||||
);
|
||||
}}
|
||||
onRemoveTool={(id) => {
|
||||
setValue(
|
||||
'selectedTools',
|
||||
selectedTools?.filter((item) => item.pluginId !== id) || [],
|
||||
{
|
||||
shouldDirty: true
|
||||
}
|
||||
);
|
||||
removeSelectedTools(selectedTools.findIndex((item) => item.id === id));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
|
@ -338,6 +374,7 @@ const EditForm = ({
|
|||
)}
|
||||
|
||||
<ConfirmModal />
|
||||
<SkillModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import type {
|
||||
SkillOptionItemType,
|
||||
SkillItemType
|
||||
|
|
@ -8,20 +7,16 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
|||
import { useTranslation } from 'next-i18next';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
checkNeedsUserConfiguration,
|
||||
getToolConfigStatus,
|
||||
validateToolConfiguration
|
||||
} from '@fastgpt/global/core/app/formEdit/utils';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { workflowStartNodeId } from '@/web/core/app/constants';
|
||||
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import type { SkillLabelItemType } from '@fastgpt/web/components/common/Textarea/PromptEditor/plugins/SkillLabelPlugin';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type';
|
||||
import type { SelectedToolItemType } from '@fastgpt/global/core/app/formEdit/type';
|
||||
import {
|
||||
getAppToolTemplates,
|
||||
getToolPreviewNode,
|
||||
|
|
@ -46,15 +41,15 @@ const isSubApp = (flowNodeType: FlowNodeTypeEnum) => {
|
|||
return !!subAppTypeMap[flowNodeType];
|
||||
};
|
||||
|
||||
export type SelectedToolItemType = AppFormEditFormType['selectedTools'][number];
|
||||
|
||||
export const useSkillManager = ({
|
||||
topAgentSelectedTools,
|
||||
selectedTools,
|
||||
onUpdateOrAddTool,
|
||||
onDeleteTool,
|
||||
canSelectFile,
|
||||
canSelectImg
|
||||
}: {
|
||||
topAgentSelectedTools: SelectedToolItemType[];
|
||||
selectedTools: SelectedToolItemType[];
|
||||
onDeleteTool: (id: string) => void;
|
||||
onUpdateOrAddTool: (tool: SelectedToolItemType) => void;
|
||||
|
|
@ -70,26 +65,33 @@ export const useSkillManager = ({
|
|||
const data = await getAppToolTemplates({ getAll: true }).catch((err) => {
|
||||
return [];
|
||||
});
|
||||
return data.map<SkillItemType>((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
parentId: item.parentId,
|
||||
label: item.name,
|
||||
icon: item.avatar,
|
||||
showArrow: item.isFolder,
|
||||
canClick: true
|
||||
};
|
||||
});
|
||||
return data
|
||||
.map<SkillItemType>((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
parentId: item.parentId,
|
||||
label: item.name,
|
||||
icon: item.avatar,
|
||||
description: item.intro,
|
||||
showArrow: item.isFolder,
|
||||
canClick: true,
|
||||
tools: data
|
||||
.filter((tool) => tool.parentId === item.id)
|
||||
.map((tool) => ({
|
||||
id: tool.id,
|
||||
name: tool.name
|
||||
}))
|
||||
};
|
||||
})
|
||||
.filter((item) => !item.parentId);
|
||||
},
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
const onLoadSystemTool = useCallback(
|
||||
async ({ parentId = null }: { parentId?: ParentIdType; searchKey?: string }) => {
|
||||
return systemTools.filter((tool) => {
|
||||
return tool.parentId === parentId;
|
||||
});
|
||||
async ({}: { searchKey?: string }) => {
|
||||
return systemTools;
|
||||
},
|
||||
[systemTools]
|
||||
);
|
||||
|
|
@ -145,19 +147,21 @@ export const useSkillManager = ({
|
|||
}, []);
|
||||
|
||||
const onAddAppOrTool = useCallback(
|
||||
async (appId: string) => {
|
||||
const toolTemplate = await getToolPreviewNode({ appId });
|
||||
|
||||
if (!toolTemplate) {
|
||||
return;
|
||||
async (toolId: string) => {
|
||||
// Check tool exists, if exists, not update/add tool
|
||||
const existsTool = selectedTools.find((tool) => tool.pluginId === toolId);
|
||||
if (existsTool) {
|
||||
return existsTool.id;
|
||||
}
|
||||
|
||||
const checkRes = validateToolConfiguration({
|
||||
const toolTemplate = await getToolPreviewNode({ appId: toolId });
|
||||
|
||||
const toolValid = validateToolConfiguration({
|
||||
toolTemplate,
|
||||
canSelectFile,
|
||||
canSelectImg
|
||||
});
|
||||
if (!checkRes) {
|
||||
if (!toolValid) {
|
||||
toast({
|
||||
title: t('app:simple_tool_tips'),
|
||||
status: 'warning'
|
||||
|
|
@ -165,19 +169,20 @@ export const useSkillManager = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// 添加与 top 相同工具的配置
|
||||
const topTool = topAgentSelectedTools.find((tool) => tool.pluginId === toolTemplate.pluginId);
|
||||
if (topTool) {
|
||||
toolTemplate.inputs.forEach((input) => {
|
||||
const topInput = topTool.inputs.find((tInput) => tInput.key === input.key);
|
||||
if (topInput) {
|
||||
input.value = topInput.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const tool: SelectedToolItemType = {
|
||||
...toolTemplate,
|
||||
id: `tool_${getNanoid(6)}`,
|
||||
inputs: toolTemplate.inputs.map((input) => {
|
||||
// 如果是文件上传类型,设置为从工作流开始节点获取用户文件
|
||||
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)) {
|
||||
return {
|
||||
...input,
|
||||
value: [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]]
|
||||
};
|
||||
}
|
||||
return input;
|
||||
})
|
||||
id: toolId
|
||||
};
|
||||
|
||||
onUpdateOrAddTool({
|
||||
|
|
@ -187,7 +192,7 @@ export const useSkillManager = ({
|
|||
|
||||
return tool.id;
|
||||
},
|
||||
[canSelectFile, canSelectImg, onUpdateOrAddTool, t, toast]
|
||||
[canSelectFile, canSelectImg, onUpdateOrAddTool, selectedTools, t, toast, topAgentSelectedTools]
|
||||
);
|
||||
|
||||
/* ===== Skill option ===== */
|
||||
|
|
@ -195,21 +200,9 @@ export const useSkillManager = ({
|
|||
return {
|
||||
onSelect: async (id: string) => {
|
||||
if (id === 'systemTool') {
|
||||
const data = await onLoadSystemTool({ parentId: null });
|
||||
const data = await onLoadSystemTool({});
|
||||
return {
|
||||
description: t('app:can_select_toolset'),
|
||||
list: data,
|
||||
onSelect: async (id: string) => {
|
||||
const data = await onLoadSystemTool({ parentId: id });
|
||||
return {
|
||||
onClick: onAddAppOrTool,
|
||||
list: data.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
canClick: true
|
||||
}))
|
||||
};
|
||||
},
|
||||
onClick: onAddAppOrTool
|
||||
};
|
||||
} else if (id === 'myTools') {
|
||||
|
|
@ -277,8 +270,8 @@ export const useSkillManager = ({
|
|||
if (!tool) return;
|
||||
|
||||
if (isSubApp(tool.flowNodeType)) {
|
||||
const { needConfig } = getToolConfigStatus(tool);
|
||||
if (!needConfig) return;
|
||||
const hasFormInput = checkNeedsUserConfiguration(tool);
|
||||
if (!hasFormInput) return;
|
||||
setConfigTool(tool);
|
||||
} else {
|
||||
console.log('onClickSkill', id);
|
||||
|
|
|
|||
|
|
@ -308,14 +308,20 @@ const RenderList = React.memo(function RenderList({
|
|||
key={template.id}
|
||||
label={
|
||||
<Box py={2} minW={['auto', '250px']}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex alignItems={'center'} w={'100%'}>
|
||||
<MyAvatar
|
||||
src={template.avatar}
|
||||
w={'1.75rem'}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
<Box fontWeight={'bold'} ml={3} color={'myGray.900'} overflow={'hidden'}>
|
||||
<Box
|
||||
fontWeight={'bold'}
|
||||
ml={3}
|
||||
color={'myGray.900'}
|
||||
flex={'1 0 0'}
|
||||
overflow={'hidden'}
|
||||
>
|
||||
{t(parseI18nString(template.name, i18n.language))}
|
||||
</Box>
|
||||
{isSystemTool && (
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ async function handler(
|
|||
|
||||
return {
|
||||
...toolNode,
|
||||
id: toolNode.pluginId!,
|
||||
inputs: mergedInputs
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type { OnOptimizePromptProps } from '@/components/common/PromptEditor/Opt
|
|||
import type { OnOptimizeCodeProps } from '@/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeCode/Copilot';
|
||||
import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
import type { TopAgentFormDataType } from '@fastgpt/service/core/chat/HelperBot/dispatch/topAgent/type';
|
||||
import type { UserInputInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
|
||||
type StreamFetchProps = {
|
||||
url?: string;
|
||||
|
|
@ -52,7 +53,15 @@ type ResponseQueueItemType = CommonResponseType &
|
|||
tools: any;
|
||||
}
|
||||
| {
|
||||
event: SseResponseEventEnum.formData;
|
||||
event: SseResponseEventEnum.collectionForm;
|
||||
collectionForm: UserInputInteractive;
|
||||
}
|
||||
| {
|
||||
event: SseResponseEventEnum.generatedSkill;
|
||||
data: any;
|
||||
}
|
||||
| {
|
||||
event: SseResponseEventEnum.topAgentConfig;
|
||||
data: TopAgentFormDataType;
|
||||
}
|
||||
);
|
||||
|
|
@ -274,7 +283,12 @@ export const streamFetch = ({
|
|||
event,
|
||||
agentPlan: rest.agentPlan
|
||||
});
|
||||
} else if (event === SseResponseEventEnum.formData) {
|
||||
} else if (event === SseResponseEventEnum.collectionForm) {
|
||||
onMessage({
|
||||
event,
|
||||
collectionForm: rest
|
||||
});
|
||||
} else if (event === SseResponseEventEnum.topAgentConfig) {
|
||||
// Directly call onMessage for formData, no need to queue
|
||||
onMessage({
|
||||
event,
|
||||
|
|
|
|||
Loading…
Reference in New Issue