skill editor ui

This commit is contained in:
archer 2025-12-11 14:23:22 +08:00
parent 27b8d7a1f0
commit 0ea6d2ee3f
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
28 changed files with 734 additions and 112 deletions

View File

@ -1 +0,0 @@
export type AgentSubAppItemType = {};

View File

@ -0,0 +1,6 @@
import { ObjectIdSchema } from '../../../../global/common/type/mongo';
import { z } from 'zod';
export type AgentSubAppItemType = {};
/* ===== Dataset ==== */

View File

@ -100,6 +100,19 @@ export type AppDatasetSearchParamsType = {
datasetSearchExtensionBg?: string;
};
/* ===== skill ===== */
export type SkillEditType = {
id: string;
name: string;
description: string;
prompt: string;
dataset: {
list: SelectedDatasetType[];
};
selectedTools: SelectedToolItemType[];
fileSelectConfig: AppFileSelectConfigType;
};
export type SelectedToolItemType = FlowNodeTemplateType & {
configStatus?: 'active' | 'waitingForConfig' | 'invalid';
};
@ -126,6 +139,7 @@ export type AppFormEditFormType = {
} & AppDatasetSearchParamsType;
selectedTools: SelectedToolItemType[];
chatConfig: AppChatConfigType;
skills: SkillEditType[];
};
export type HttpToolConfigType = {

View File

@ -28,7 +28,8 @@ export const getDefaultAppForm = (): AppFormEditFormType => {
datasetSearchExtensionBg: ''
},
selectedTools: [],
chatConfig: {}
chatConfig: {},
skills: []
};
};

View File

@ -4,7 +4,8 @@ import { ChatRoleEnum } from '../constants';
import { UserChatItemSchema, SystemChatItemSchema, ToolModuleResponseItemSchema } from '../type';
export enum HelperBotTypeEnum {
topAgent = 'topAgent'
topAgent = 'topAgent',
skillEditor = 'skillEditor'
}
export const HelperBotTypeEnumSchema = z.enum(Object.values(HelperBotTypeEnum));
export type HelperBotTypeEnumType = z.infer<typeof HelperBotTypeEnumSchema>;
@ -72,7 +73,7 @@ export type HelperBotChatItemSiteType = z.infer<typeof HelperBotChatItemSiteSche
/* 具体的 bot 的特有参数 */
// AI 模型配置
// Top agent
export const topAgentParamsSchema = z.object({
role: z.string().nullish(),
taskObject: z.string().nullish(),
@ -81,3 +82,7 @@ export const topAgentParamsSchema = z.object({
fileUpload: z.boolean().nullish()
});
export type TopAgentParamsType = z.infer<typeof topAgentParamsSchema>;
// Skill editor
export const skillEditorParamsSchema = z.object({});
export type SkillEditorParamsType = z.infer<typeof skillEditorParamsSchema>;

View File

@ -172,6 +172,7 @@ export enum NodeInputKeyEnum {
// agent
subApps = 'subApps',
skills = 'skills',
isAskAgent = 'isAskAgent',
isPlanAgent = 'isPlanAgent',
isConfirmPlanAgent = 'isConfirmPlanAgent',

View File

@ -3,6 +3,7 @@ import {
type HelperBotChatItemSiteType,
HelperBotTypeEnum,
HelperBotTypeEnumSchema,
skillEditorParamsSchema,
topAgentParamsSchema
} from '../../../../core/chat/helperBot/type';
import { z } from 'zod';
@ -58,6 +59,10 @@ export const HelperBotCompletionsParamsSchema = z.object({
z.object({
type: z.literal(HelperBotTypeEnum.topAgent),
data: topAgentParamsSchema
}),
z.object({
type: z.literal(HelperBotTypeEnum.skillEditor),
data: skillEditorParamsSchema
})
])
});

View File

@ -1,6 +1,8 @@
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import { dispatchTopAgent } from './topAgent';
import { dispatchSkillEditor } from './skillEditor';
export const dispatchMap = {
[HelperBotTypeEnum.topAgent]: dispatchTopAgent
[HelperBotTypeEnum.topAgent]: dispatchTopAgent,
[HelperBotTypeEnum.skillEditor]: dispatchSkillEditor
};

View File

@ -0,0 +1,15 @@
import type { HelperBotDispatchParamsType, HelperBotDispatchResponseType } from '../type';
export const dispatchSkillEditor = async (
props: HelperBotDispatchParamsType
): Promise<HelperBotDispatchResponseType> => {
console.log(props, 22222);
return {
aiResponse: [],
usage: {
model: '',
inputTokens: 0,
outputTokens: 0
}
};
};

View File

@ -9,11 +9,12 @@ import { generateResourceList } from './utils';
import { TopAgentFormDataSchema } from './type';
import { addLog } from '../../../../../common/system/log';
import { formatAIResponse } from '../utils';
import type { TopAgentParamsType } from '@fastgpt/global/core/chat/helperBot/type';
export const dispatchTopAgent = async (
props: HelperBotDispatchParamsType
props: HelperBotDispatchParamsType<TopAgentParamsType>
): Promise<HelperBotDispatchResponseType> => {
const { query, files, metadata, histories, workflowResponseWrite, user } = props;
const { query, files, data, histories, workflowResponseWrite, user } = props;
const modelData = getLLMModel();
if (!modelData) {
@ -32,7 +33,7 @@ export const dispatchTopAgent = async (
});
const systemPrompt = getPrompt({
resourceList,
metadata: metadata.data
metadata: data
});
const historyMessages = helperChats2GPTMessages({

View File

@ -10,7 +10,7 @@ import { LocaleList } from '@fastgpt/global/common/i18n/type';
export const HelperBotDispatchParamsSchema = z.object({
query: z.string(),
files: HelperBotCompletionsParamsSchema.shape.files,
metadata: HelperBotCompletionsParamsSchema.shape.metadata,
data: z.unknown(), // Allow any type, will be constrained by generic type parameter
histories: z.array(HelperBotChatItemSchema),
workflowResponseWrite: WorkflowResponseFnSchema,
@ -22,7 +22,14 @@ export const HelperBotDispatchParamsSchema = z.object({
lang: z.enum(LocaleList)
})
});
export type HelperBotDispatchParamsType = z.infer<typeof HelperBotDispatchParamsSchema>;
type BaseHelperBotDispatchParamsType = z.infer<typeof HelperBotDispatchParamsSchema>;
export type HelperBotDispatchParamsType<T = unknown> = Omit<
BaseHelperBotDispatchParamsType,
'data'
> & {
data: T;
};
export const HelperBotDispatchResponseSchema = z.object({
aiResponse: z.array(AIChatItemValueItemSchema),

View File

@ -327,6 +327,12 @@
"show_templates": "Expand",
"show_top_p_tip": "An alternative method of temperature sampling, called Nucleus sampling, the model considers the results of tokens with TOP_P probability mass quality. \nTherefore, 0.1 means that only tokens containing the highest probability quality are considered. \nThe default is 1.",
"simple_tool_tips": "This tool contains special inputs and does not support being called by simple applications.",
"skill_description_placeholder": "Used to guide the Agent to select the skill for execution",
"skill_editor": "Skill-assisted generation",
"skill_empty_name": "Unnamed skill",
"skill_name_placeholder": "Please enter the skill description, for display only",
"skills": "Skills",
"skills_tip": "Model behavioral knowledge",
"source_updateTime": "Update time",
"space_to_expand_folder": "Press \"Space\" to expand the folder",
"stop_sign": "Stop",

View File

@ -840,6 +840,7 @@
"delete_folder": "Delete Folder",
"delete_success": "Deleted Successfully",
"delete_warning": "Deletion Warning",
"descripton": "describe",
"discount_coupon_used": "Coupon used:",
"embedding_model_not_config": "No index model is detected",
"enable_auth": "Enable authentication",

View File

@ -157,6 +157,7 @@
"export_config_successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据",
"export_configs": "导出配置",
"export_log_filename": "{{name}} 对话日志.csv",
"failed_tools": "失败的工具",
"fastgpt_marketplace": "FastGPT 插件市场",
"feedback_count": "用户反馈",
"file_quote_link": "文件链接",
@ -340,6 +341,12 @@
"show_templates": "显示模板",
"show_top_p_tip": "用温度采样的替代方法称为Nucleus采样该模型考虑了具有TOP_P概率质量质量的令牌的结果。因此0.1表示仅考虑包含最高概率质量的令牌。默认为 1。",
"simple_tool_tips": "该工具含有特殊输入,暂不支持被简易应用调用",
"skill_description_placeholder": "用于引导 Agent 选中该技能进行执行",
"skill_editor": "技能辅助生成",
"skill_empty_name": "未命名的技能",
"skill_name_placeholder": "请输入技能明,仅用于展示",
"skills": "技能",
"skills_tip": "模型的行为知识",
"source_updateTime": "更新时间",
"space_to_expand_folder": "按\"空格\"展开文件夹",
"stop_sign": "停止序列",
@ -396,6 +403,7 @@
"tool_active_system_config_price_desc_folder": "需额外支付密钥价格,依据实际使用工具扣费。",
"tool_detail": "工具详情",
"tool_input_param_tip": "该工具正常运行需要配置相关信息",
"tool_load_failed": "部分工具加载失败",
"tool_not_active": "该工具尚未激活",
"tool_offset_tips": "该工具已无法使用,将中断应用运行,请立即替换",
"tool_param_config": "参数配置",
@ -468,8 +476,6 @@
"toolkit_uninstalled": "未安装",
"toolkit_update_failed": "更新失败",
"toolkit_user_guide": "使用说明",
"tool_load_failed": "部分工具加载失败",
"failed_tools": "失败的工具",
"tools": "工具",
"tools_no_description": "这个工具没有介绍~",
"tools_tip": "声明模型可用的工具,可以实现与外部系统交互等扩展能力",

View File

@ -845,6 +845,7 @@
"delete_folder": "删除文件夹",
"delete_success": "删除成功",
"delete_warning": "删除警告",
"descripton": "描述",
"discount_coupon_used": "已使用优惠券:",
"embedding_model_not_config": "检测到没有可用的索引模型",
"enable_auth": "启用鉴权",

View File

@ -325,6 +325,12 @@
"show_templates": "顯示模板",
"show_top_p_tip": "用溫度取樣的替代方法,稱為 Nucleus 取樣,該模型考慮了具有 TOP_P 機率質量質量的令牌的結果。\n因此0.1 表示僅考慮包含最高機率質量的令牌。\n預設為 1。",
"simple_tool_tips": "該工具含有特殊輸入,暫不支持被簡易應用調用",
"skill_description_placeholder": "用於引導 Agent 選中該技能進行執行",
"skill_editor": "技能輔助生成",
"skill_empty_name": "未命名的技能",
"skill_name_placeholder": "請輸入技能明,僅用於展示",
"skills": "技能",
"skills_tip": "模型的行為知識",
"source_updateTime": "更新時間",
"space_to_expand_folder": "按\"空格\"展開文件夾",
"stop_sign": "停止序列",

View File

@ -839,6 +839,7 @@
"delete_folder": "刪除資料夾",
"delete_success": "刪除成功",
"delete_warning": "刪除警告",
"descripton": "描述",
"discount_coupon_used": "已使用優惠券:",
"embedding_model_not_config": "偵測到沒有可用的索引模型",
"enable_auth": "啟用鑑權",

View File

@ -334,7 +334,7 @@ const ChatBox = ({ type, metadata, onApply, ...props }: HelperBotProps) => {
name: item.name
})),
metadata: {
type: 'topAgent',
type,
data: metadata
}
},

View File

@ -1,16 +1,19 @@
import React, { useState } from 'react';
import { Box } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import ChatTest from './ChatTest';
import AppCard from '../FormComponent/AppCard';
import EditForm from './EditForm';
import { type AppFormEditFormType } from '@fastgpt/global/core/app/type';
import type { SkillEditType, AppFormEditFormType } from '@fastgpt/global/core/app/type';
import { cardStyles } from '../../constants';
import styles from '../FormComponent/styles.module.scss';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { type SimpleAppSnapshotType } from '../FormComponent/useSnapshots';
import { agentForm2AppWorkflow } from './utils';
import styles from '../FormComponent/styles.module.scss';
import dynamic from 'next/dynamic';
const SkillEditForm = dynamic(() => import('./SkillEdit/EditForm'), { ssr: false });
const SKillChatTest = dynamic(() => import('./SkillEdit/ChatTest'), { ssr: false });
const Edit = ({
appForm,
@ -23,6 +26,7 @@ const Edit = ({
}) => {
const { isPc } = useSystem();
const [renderEdit, setRenderEdit] = useState(true);
const [editSkill, setEditSkill] = useState<SkillEditType>();
return (
<Box
@ -33,7 +37,9 @@ const Edit = ({
gap={1}
borderRadius={'lg'}
overflowY={['auto', 'unset']}
position={'relative'}
>
{/* Top agent editor */}
{renderEdit && (
<Box
className={styles.EditAppBox}
@ -47,7 +53,11 @@ const Edit = ({
</Box>
<Box pb={4}>
<EditForm appForm={appForm} setAppForm={setAppForm} />
<EditForm
appForm={appForm}
setAppForm={setAppForm}
onEditSkill={(e) => setEditSkill(e)}
/>
</Box>
</Box>
)}
@ -61,6 +71,52 @@ const Edit = ({
/>
</Box>
)}
{/* Mask */}
{editSkill && (
<Box
position={'absolute'}
top={0}
left={0}
right={0}
bottom={0}
bg={'rgba(0, 0, 0, 0.5)'}
borderRadius={'md'}
zIndex={9}
></Box>
)}
{/* Skill editor */}
<Flex
position={'absolute'}
top={0}
left={0}
right={0}
bottom={0}
bg={'white'}
borderRadius={'md'}
zIndex={10}
transform={editSkill ? 'translateX(0)' : 'translateX(100%)'}
transition={'transform 0.3s ease-in-out'}
pointerEvents={editSkill ? 'auto' : 'none'}
>
{editSkill && (
<>
<Box overflowY={'auto'} minW={['auto', '580px']} flex={'1'} borderRight={'base'}>
<SkillEditForm
model={appForm.aiSettings.model}
fileSelectConfig={appForm.chatConfig.fileSelectConfig}
defaultSkill={editSkill}
onClose={() => setEditSkill(undefined)}
setAppForm={setAppForm}
/>
</Box>
<Box flex={'2 0 0'} w={0} mb={3}>
<SKillChatTest skill={editSkill} setAppForm={setAppForm} />
</Box>
</>
)}
</Flex>
</Box>
);
};

View File

@ -10,7 +10,7 @@ import {
HStack,
Input
} from '@chakra-ui/react';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/type.d';
import type { AppFormEditFormType, SkillEditType } from '@fastgpt/global/core/app/type.d';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
@ -20,23 +20,17 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import VariableEdit from '@/components/core/app/VariableEdit';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import { TTSTypeEnum } from '@/web/core/app/constants';
import { workflowSystemVariables } from '@/web/core/app/utils';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pageComponents/app/detail/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
import { getWebLLMModel } from '@/web/common/system/utils';
import ToolSelect from '../FormComponent/ToolSelector/ToolSelect';
import OptimizerPopover from '@/components/common/PromptEditor/OptimizerPopover';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { type SelectedToolItemType, useSkillManager } from './hooks/useSkillManager';
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
import SkillRow from './SkillEdit/Row';
import { cardStyles } from '../../constants';
import { SmallAddIcon } from '@chakra-ui/icons';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
@ -56,10 +50,12 @@ const BoxStyles: BoxProps = {
const EditForm = ({
appForm,
setAppForm
setAppForm,
onEditSkill
}: {
appForm: AppFormEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>;
onEditSkill: (e: SkillEditType) => void;
}) => {
const theme = useTheme();
const router = useRouter();
@ -80,26 +76,6 @@ const EditForm = ({
onClose: onCloseDatasetParams
} = useDisclosure();
const formatVariables = useMemo(
() =>
formatEditorVariablePickerIcon([
...workflowSystemVariables.filter(
(variable) =>
!['appId', 'chatId', 'responseChatItemId', 'histories'].includes(variable.key)
),
...(appForm.chatConfig.variables || [])
]).map((item) => ({
...item,
label: t(item.label as any),
parent: {
id: 'VARIABLE_NODE_ID',
label: t('common:core.module.Variable'),
avatar: 'core/workflow/template/variable'
}
})),
[appForm.chatConfig.variables, t]
);
const selectedModel = getWebLLMModel(appForm.aiSettings.model);
const tokenLimit = useMemo(() => {
return selectedModel?.quoteMaxToken || 3000;
@ -226,9 +202,44 @@ const EditForm = ({
</Box>
</Box>
<Box {...BoxStyles}>
<SkillRow
skills={appForm.skills}
onEditSkill={onEditSkill}
onDeleteSkill={(id) => {
setAppForm((state) => ({
...state,
skills: state.skills.filter((item) => item.id !== id)
}));
}}
/>
</Box>
{/* tool choice */}
<Box {...BoxStyles}>
<ToolSelect appForm={appForm} setAppForm={setAppForm} />
<ToolSelect
selectedModel={selectedModel}
selectedTools={appForm.selectedTools}
fileSelectConfig={appForm.chatConfig.fileSelectConfig}
onAddTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: [e, ...(state.selectedTools || [])]
}));
}}
onUpdateTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools:
state.selectedTools?.map((item) => (item.id === e.id ? e : item)) || []
}));
}}
onRemoveTool={(id) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools?.filter((item) => item.id !== id) || []
}));
}}
/>
</Box>
{/* dataset */}
@ -239,17 +250,6 @@ const EditForm = ({
<FormLabel ml={2}>{t('app:dataset')}</FormLabel>
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<MyIcon name="common/addLight" w={'0.8rem'} />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenKbSelect}
>
{t('common:Choose')}
</Button>
<Button
mr={'-5px'}
variant={'transparentBase'}
leftIcon={<MyIcon name={'edit'} w={'14px'} />}
iconSpacing={1}
@ -259,6 +259,17 @@ const EditForm = ({
>
{t('common:Params')}
</Button>
<Button
mr={'-5px'}
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenKbSelect}
>
{t('common:Choose')}
</Button>
</Flex>
{appForm.dataset.datasets?.length > 0 && (
<Box my={3}>

View File

@ -0,0 +1,59 @@
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useMemo } from 'react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { AppFormEditFormType, SkillEditType } from '@fastgpt/global/core/app/type';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../../../context';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { cardStyles } from '../../../constants';
import HelperBot from '@/components/core/chat/HelperBot';
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
type Props = {
skill: SkillEditType;
setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>;
};
const ChatTest = ({ skill, setAppForm }: Props) => {
const { t } = useTranslation();
const { toast } = useToast();
// 构建 SkillAgent metadata,从 appForm 中提取配置
const skillAgentMetadata = useMemo(() => ({}), []);
return (
<MyBox display={'flex'} position={'relative'} flexDirection={'column'} h={'full'} py={4}>
<Flex px={[2, 5]} pb={2}>
<Box color={'myGray.900'} fontWeight={'bold'} flex={1}>
{t('app:skill_editor')}
</Box>
<MyTooltip label={t('common:core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
}}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<HelperBot
type={HelperBotTypeEnum.skillEditor}
metadata={skillAgentMetadata}
onApply={(e) => {
console.log(e);
}}
/>
</Box>
</MyBox>
);
};
export default React.memo(ChatTest);

View File

@ -0,0 +1,274 @@
import React, { useEffect, useMemo, useTransition } from 'react';
import {
Box,
Flex,
Grid,
type BoxProps,
useTheme,
useDisclosure,
Button,
HStack,
Input,
IconButton,
Textarea
} from '@chakra-ui/react';
import type {
AppFileSelectConfigType,
AppFormEditFormType,
SkillEditType
} from '@fastgpt/global/core/app/type.d';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
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 VariableEdit from '@/components/core/app/VariableEdit';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import { TTSTypeEnum } from '@/web/core/app/constants';
import { workflowSystemVariables } from '@/web/core/app/utils';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pageComponents/app/detail/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
import { getWebLLMModel } from '@/web/common/system/utils';
import ToolSelect from '../../FormComponent/ToolSelector/ToolSelect';
import OptimizerPopover from '@/components/common/PromptEditor/OptimizerPopover';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { type SelectedToolItemType, useSkillManager } from '../hooks/useSkillManager';
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
import { cardStyles } from '../../../constants';
import { defaultSkill as defaultEditSkill } from './Row';
import { useForm } from 'react-hook-form';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import { SmallAddIcon } from '@chakra-ui/icons';
import { getNanoid } from '@fastgpt/global/common/string/tools';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
const QGConfig = dynamic(() => import('@/components/core/app/QGConfig'));
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
const InputGuideConfig = dynamic(() => import('@/components/core/app/InputGuideConfig'));
const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig'));
const FileSelectConfig = dynamic(() => import('@/components/core/app/FileSelect'));
const EditForm = ({
model,
fileSelectConfig,
defaultSkill = defaultEditSkill,
onClose,
setAppForm
}: {
model: string;
fileSelectConfig?: AppFileSelectConfigType;
defaultSkill?: SkillEditType;
onClose: () => void;
setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>;
}) => {
const theme = useTheme();
const router = useRouter();
const { t } = useTranslation();
const [, startTst] = useTransition();
const selectedModel = getWebLLMModel(model);
const { register, setValue, handleSubmit, reset, watch } = useForm<SkillEditType>({
defaultValues: defaultSkill
});
useEffect(() => {
reset(defaultSkill);
}, [defaultSkill, reset]);
const name = watch('name');
const prompt = watch('prompt');
const selectedTools = watch('selectedTools');
const selectDatasets = watch('dataset.list');
const {
isOpen: isOpenDatasetSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const onSave = (e: SkillEditType) => {
setAppForm((state) => ({
...state,
skills: e.id
? state.skills.map((item) => (item.id === e.id ? e : item))
: [{ ...e, id: getNanoid(6) }, ...state.skills]
}));
onClose();
};
return (
<>
<Box p={5}>
{/* Header */}
<HStack gap={4}>
<IconButton
variant={'whiteBase'}
icon={<MyIcon name="common/backLight" w={'1rem'} />}
size={'smSquare'}
aria-label={''}
w={'32px'}
h={'32px'}
onClick={onClose}
/>
<Box color={'myGray.900'} flex={'1 0 0'} w={'0'} className={'textEllipsis'}>
{name || t('app:skill_empty_name')}
</Box>
<Button variant={'primary'} onClick={handleSubmit(onSave)}>
{t('common:Save')}
</Button>
</HStack>
{/* Name */}
<HStack mt={5}>
<FormLabel mr={3} required>
{t('common:Name')}
</FormLabel>
<Input
{...register('name', { required: true })}
maxLength={30}
placeholder={t('app:skill_name_placeholder')}
/>
</HStack>
{/* Desc */}
<Box mt={4}>
<HStack>
<FormLabel mr={1} required>
{t('common:descripton')}
</FormLabel>
<QuestionTip label={t('app:skill_description_placeholder')} />
</HStack>
<Textarea
rows={3}
mt={1}
resize={'vertical'}
{...register('description', { required: true })}
placeholder={t('app:skill_description_placeholder')}
/>
</Box>
{/* Prompt */}
<Box mt={4}>
<HStack w={'100%'}>
<FormLabel>Prompt</FormLabel>
</HStack>
<Box mt={1}>
<PromptEditor
minH={100}
maxH={300}
value={prompt}
onChange={(text) => {
startTst(() => {
setValue('prompt', text);
});
}}
isRichText={false}
/>
</Box>
</Box>
{/* Tool select */}
<Box mt={5} px={3} py={4} borderTop={'base'}>
<ToolSelect
selectedModel={selectedModel}
selectedTools={selectedTools}
fileSelectConfig={fileSelectConfig}
onAddTool={(e) => {
setValue('selectedTools', [e, ...(selectedTools || [])]);
}}
onUpdateTool={(e) => {
setValue(
'selectedTools',
selectedTools?.map((item) => (item.id === e.id ? e : item)) || []
);
}}
onRemoveTool={(id) => {
setValue('selectedTools', selectedTools?.filter((item) => item.id !== id) || []);
}}
/>
</Box>
{/* Dataset select */}
<Box py={4} px={3} borderTop={'base'} borderBottom={'base'}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/simpleMode/dataset'} w={'20px'} />
<FormLabel ml={2}>{t('app:dataset')}</FormLabel>
</Flex>
<Button
mr={'-5px'}
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenKbSelect}
>
{t('common:Choose')}
</Button>
</Flex>
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
{selectDatasets.map((item) => (
<MyTooltip key={item.datasetId} label={t('common:core.dataset.Read Dataset')}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
cursor={'pointer'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item.datasetId
}
})
}
>
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
{item.name}
</Box>
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
</Box>
{isOpenDatasetSelect && (
<DatasetSelectModal
defaultSelectedDatasets={selectDatasets.map((item) => ({
datasetId: item.datasetId,
vectorModel: item.vectorModel,
name: item.name,
avatar: item.avatar
}))}
onClose={onCloseKbSelect}
onChange={(e) => {
setValue('dataset.list', e);
}}
/>
)}
</>
);
};
export default React.memo(EditForm);

View File

@ -0,0 +1,88 @@
import React from 'react';
import { Box, Button, Flex, Grid, HStack, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { SmallAddIcon } from '@chakra-ui/icons';
import { useTranslation } from 'next-i18next';
import type { SkillEditType } from '@fastgpt/global/core/app/type';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
export const defaultSkill: SkillEditType = {
id: '',
name: '',
description: '',
prompt: '',
dataset: {
list: []
},
selectedTools: [],
fileSelectConfig: {
canSelectFile: false,
canSelectImg: false,
canSelectVideo: false,
canSelectAudio: false,
canSelectCustomFileExtension: false,
customFileExtensionList: []
}
};
const Row = ({
skills,
onEditSkill,
onDeleteSkill
}: {
skills: SkillEditType[];
onEditSkill: (e: SkillEditType) => void;
onDeleteSkill: (id: string) => void;
}) => {
const { t } = useTranslation();
return (
<Box>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/toolCall'} w={'20px'} />
<FormLabel ml={2}>{t('app:skills')}</FormLabel>
<QuestionTip ml={1} label={t('app:skills_tip')} />
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
mr={'-5px'}
size={'sm'}
fontSize={'sm'}
onClick={() => onEditSkill({ ...defaultSkill })}
>
{t('common:Add')}
</Button>
</Flex>
<Box mt={3}>
{skills.map((skill) => (
<HStack
key={skill.id}
justifyContent={'space-between'}
py={2}
px={4}
borderRadius={'md'}
border={'base'}
_notLast={{
mb: 2
}}
_hover={{
bg: 'myGray.25'
}}
>
<Box flex={'1 0 0'}>{skill.name}</Box>
<MyIconButton icon={'edit'} onClick={() => onEditSkill(skill)} />
<MyIconButton icon={'delete'} onClick={() => onDeleteSkill(skill.id)} />
</HStack>
))}
</Box>
</Box>
);
};
export default Row;

View File

@ -1,4 +1,8 @@
import type { AppChatConfigType, AppFormEditFormType } from '@fastgpt/global/core/app/type';
import type {
AppChatConfigType,
AppFormEditFormType,
SkillEditType
} from '@fastgpt/global/core/app/type';
import type {
FlowNodeTemplateType,
StoreNodeItemType
@ -51,11 +55,14 @@ export const appWorkflow2AgentForm = ({
defaultAppForm.aiSettings.aiChatTopP = inputMap.get(NodeInputKeyEnum.aiChatTopP);
const subApps = inputMap.get(NodeInputKeyEnum.subApps) as FlowNodeTemplateType[];
if (subApps) {
subApps.forEach((subApp) => {
defaultAppForm.selectedTools.push(subApp);
});
defaultAppForm.selectedTools = subApps;
}
// TODO: 临时存这里,后续会改成单独表存储
const skills = inputMap.get(NodeInputKeyEnum.skills) as SkillEditType[];
if (skills) {
defaultAppForm.skills = skills;
}
} else if (node.flowNodeType === FlowNodeTypeEnum.systemConfig) {
defaultAppForm.chatConfig = getAppChatConfig({
@ -187,7 +194,7 @@ export function agentForm2AppWorkflow(
key: NodeInputKeyEnum.subApps,
renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window
label: '',
valueType: WorkflowIOValueTypeEnum.object,
valueType: WorkflowIOValueTypeEnum.arrayObject,
value: data.selectedTools.map((tool) => ({
...tool,
inputs: tool.inputs.map((input) => {
@ -211,6 +218,38 @@ export function agentForm2AppWorkflow(
return input;
})
}))
},
{
key: NodeInputKeyEnum.skills,
renderTypeList: [FlowNodeInputTypeEnum.hidden], // Set in the pop-up window
label: '',
valueType: WorkflowIOValueTypeEnum.arrayObject,
value: data.skills.map((skill) => ({
...skill,
selectedTools: skill.selectedTools.map((tool) => ({
...tool,
inputs: tool.inputs.map((input) => {
// Special key value
if (input.key === NodeInputKeyEnum.forbidStream) {
input.value = true;
}
// Special tool
if (
tool.flowNodeType === FlowNodeTypeEnum.appModule &&
input.key === NodeInputKeyEnum.history
) {
return {
...input,
value: data.aiSettings.maxHistories
};
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)) {
input.value = [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]];
}
return input;
})
}))
}))
}
],
outputs: AgentNode.outputs

View File

@ -4,15 +4,15 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { SmallAddIcon } from '@chakra-ui/icons';
import { type AppFormEditFormType } from '@fastgpt/global/core/app/type';
import type {
SelectedToolItemType,
AppFormEditFormType,
AppFileSelectConfigType
} from '@fastgpt/global/core/app/type';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { theme } from '@fastgpt/web/styles/theme';
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
import ToolSelectModal, { childAppSystemKey } from './ToolSelectModal';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
import ToolSelectModal from './ToolSelectModal';
import Avatar from '@fastgpt/web/components/common/Avatar';
import ConfigToolModal from '../../component/ConfigToolModal';
import { getWebLLMModel } from '@/web/common/system/utils';
@ -22,13 +22,22 @@ import { PluginStatusEnum, PluginStatusMap } from '@fastgpt/global/core/plugin/t
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { checkNeedsUserConfiguration } from '../../ChatAgent/utils';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
const ToolSelect = ({
appForm,
setAppForm
selectedModel,
selectedTools = [],
fileSelectConfig = {},
onAddTool,
onUpdateTool,
onRemoveTool
}: {
appForm: AppFormEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>;
selectedModel: LLMModelItemType;
selectedTools?: SelectedToolItemType[];
fileSelectConfig?: AppFileSelectConfigType;
onAddTool: (tool: SelectedToolItemType) => void;
onUpdateTool: (tool: SelectedToolItemType) => void;
onRemoveTool: (id: string) => void;
}) => {
const { t } = useTranslation();
@ -41,7 +50,6 @@ const ToolSelect = ({
onOpen: onOpenToolsSelect,
onClose: onCloseToolsSelect
} = useDisclosure();
const selectedModel = getWebLLMModel(appForm.aiSettings.model);
return (
<>
@ -64,11 +72,11 @@ const ToolSelect = ({
</Button>
</Flex>
<Grid
mt={appForm.selectedTools.length > 0 ? 2 : 0}
mt={selectedTools.length > 0 ? 2 : 0}
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
gridGap={[2, 4]}
>
{appForm.selectedTools.map((item) => {
{selectedTools.map((item) => {
const toolError = formatToolError(item.pluginData?.error);
// 即将下架/已下架
const status = item.status || item.pluginData?.status;
@ -161,10 +169,7 @@ const ToolSelect = ({
hoverColor="red.600"
onClick={(e) => {
e.stopPropagation();
setAppForm((state: AppFormEditFormType) => ({
...state,
selectedTools: state.selectedTools.filter((tool) => tool.id !== item.id)
}));
onRemoveTool(item.id);
}}
/>
</Flex>
@ -175,20 +180,14 @@ const ToolSelect = ({
{isOpenToolsSelect && (
<ToolSelectModal
selectedTools={appForm.selectedTools}
chatConfig={appForm.chatConfig}
selectedTools={selectedTools}
fileSelectConfig={fileSelectConfig}
selectedModel={selectedModel}
onAddTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: [...state.selectedTools, e]
}));
onAddTool(e);
}}
onRemoveTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.filter((item) => item.pluginId !== e.id)
}));
onRemoveTool(e.id);
}}
onClose={onCloseToolsSelect}
/>
@ -198,17 +197,10 @@ const ToolSelect = ({
configTool={configTool}
onCloseConfigTool={() => setConfigTool(null)}
onAddTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.map((item) =>
item.pluginId === configTool.pluginId
? {
...e,
configStatus: 'active'
}
: item
)
}));
onUpdateTool({
...e,
configStatus: 'active'
});
}}
/>
)}

View File

@ -40,7 +40,7 @@ import { checkNeedsUserConfiguration, validateToolConfiguration } from '../../Ch
type Props = {
selectedTools: FlowNodeTemplateType[];
chatConfig: AppFormEditFormType['chatConfig'];
fileSelectConfig: AppFormEditFormType['chatConfig']['fileSelectConfig'];
selectedModel: LLMModelItemType;
onAddTool: (tool: SelectedToolItemType) => void;
onRemoveTool: (tool: NodeTemplateListItemType) => void;
@ -244,7 +244,7 @@ const RenderList = React.memo(function RenderList({
onRemoveTool,
setParentId,
selectedTools,
chatConfig
fileSelectConfig
}: Props & {
templates: NodeTemplateListItemType[];
type: TemplateTypeEnum;
@ -262,8 +262,8 @@ const RenderList = React.memo(function RenderList({
const toolValid = validateToolConfiguration({
toolTemplate: res,
canSelectFile: chatConfig?.fileSelectConfig?.canSelectFile,
canSelectImg: chatConfig?.fileSelectConfig?.canSelectImg
canSelectFile: fileSelectConfig?.canSelectFile,
canSelectImg: fileSelectConfig?.canSelectImg
});
if (!toolValid) {
return toast({

View File

@ -327,7 +327,30 @@ const EditForm = ({
{/* tool choice */}
<Box {...BoxStyles}>
<ToolSelect appForm={appForm} setAppForm={setAppForm} />
<ToolSelect
selectedModel={selectedModel}
selectedTools={appForm.selectedTools}
fileSelectConfig={appForm.chatConfig.fileSelectConfig}
onAddTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: [e, ...(state.selectedTools || [])]
}));
}}
onUpdateTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools:
state.selectedTools?.map((item) => (item.id === e.id ? e : item)) || []
}));
}}
onRemoveTool={(id) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools?.filter((item) => item.id !== id) || []
}));
}}
/>
</Box>
{/* File select */}

View File

@ -40,10 +40,13 @@ async function handler(req: ApiRequestProps<completionsBody>, res: ApiResponseTy
// 执行不同逻辑
const fn = dispatchMap[metadata.type];
if (!fn) {
return Promise.reject('Invalid helper bot type');
}
const result = await fn({
query,
files,
metadata,
data: metadata.data,
histories,
workflowResponseWrite,
user: {