Plugin runtime (#2050)

* feat: plugin run (#1950)

* feat: plugin run

* fix

* ui

* fix

* change user input type

* fix

* fix

* temp

* split out plugin chat

* perf: chatbox

* perf: chatbox

* fix: plugin runtime (#2032)

* fix: plugin runtime

* fix

* fix build

* fix build

* perf: chat send prompt

* perf: chat log ux

* perf: chatbox context and share page plugin runtime

* perf: plugin run time config

* fix: ts

* feat: doc

* perf: isPc check

* perf: variable input render

* feat: app search

* fix: response box height

* fix: phone ui

* perf: lock

* perf: plugin route

* fix: chat (#2049)

---------

Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
Archer
2024-07-15 22:50:48 +08:00
committed by GitHub
parent 090c880860
commit b5c98a4f63
126 changed files with 5012 additions and 4317 deletions

View File

@@ -0,0 +1,10 @@
import { StoreNodeItemType } from '../../workflow/type/node';
import { FlowNodeInputItemType } from '../../workflow/type/io';
import { FlowNodeTypeEnum } from '../../workflow/node/constant';
export const getPluginInputsFromStoreNodes = (nodes: StoreNodeItemType[]) => {
return nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs || [];
};
export const getPluginRunContent = (e: { pluginInputs: FlowNodeInputItemType[] }) => {
return JSON.stringify(e);
};

View File

@@ -21,6 +21,7 @@ export type AppSchema = {
name: string;
avatar: string;
intro: string;
updateTime: Date;
modules: StoreNodeItemType[];

View File

@@ -56,7 +56,10 @@ export const chats2GPTMessages = ({
text: item.text?.content || ''
};
}
if (item.type === 'file' && item.file?.type === ChatFileTypeEnum.image) {
if (
item.type === ChatItemValueTypeEnum.file &&
item.file?.type === ChatFileTypeEnum.image
) {
return {
type: 'image_url',
image_url: {
@@ -64,7 +67,6 @@ export const chats2GPTMessages = ({
}
};
}
return;
})
.filter(Boolean) as ChatCompletionContentPart[];
@@ -166,7 +168,7 @@ export const GPTMessages2Chats = (
} else if (item.type === 'image_url') {
value.push({
//@ts-ignore
type: 'file',
type: ChatItemValueTypeEnum.file,
file: {
type: ChatFileTypeEnum.image,
name: '',
@@ -175,7 +177,6 @@ export const GPTMessages2Chats = (
});
}
});
// @ts-ignore
}
} else if (
obj === ChatRoleEnum.AI &&

View File

@@ -13,8 +13,8 @@ import { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants';
import { AppChatConfigType, AppSchema, VariableItemType } from '../app/type';
import type { AppSchema as AppType } from '@fastgpt/global/core/app/type.d';
import { DatasetSearchModeEnum } from '../dataset/constants';
import { ChatBoxInputType } from '../../../../projects/app/src/components/ChatBox/type';
import { DispatchNodeResponseType } from '../workflow/runtime/type.d';
import { ChatBoxInputType } from '../../../../projects/app/src/components/core/chat/ChatContainer/ChatBox/type';
export type ChatSchema = {
_id: string;
@@ -115,6 +115,7 @@ export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatIt
status: `${ChatStatusEnum}`;
moduleName?: string;
ttsBuffer?: Uint8Array;
responseData?: ChatHistoryItemResType[];
} & ChatBoxInputType;
/* --------- team chat --------- */

View File

@@ -65,11 +65,12 @@ export const filterPublicNodeResponseData = ({
}: {
flowResponses?: ChatHistoryItemResType[];
}) => {
const filedList = ['quoteList', 'moduleType'];
const filedList = ['quoteList', 'moduleType', 'pluginOutput'];
const filterModuleTypeList: any[] = [
FlowNodeTypeEnum.pluginModule,
FlowNodeTypeEnum.datasetSearchNode,
FlowNodeTypeEnum.tools
FlowNodeTypeEnum.tools,
FlowNodeTypeEnum.pluginOutput
];
return flowResponses
@@ -89,14 +90,22 @@ export const filterPublicNodeResponseData = ({
});
};
export const removeEmptyUserInput = (input: UserChatItemValueItemType[]) => {
return input.filter((item) => {
if (item.type === ChatItemValueTypeEnum.text && !item.text?.content?.trim()) {
return false;
}
if (item.type === ChatItemValueTypeEnum.file && !item.file?.url) {
return false;
}
return true;
});
export const removeEmptyUserInput = (input?: UserChatItemValueItemType[]) => {
return (
input?.filter((item) => {
if (item.type === ChatItemValueTypeEnum.text && !item.text?.content?.trim()) {
return false;
}
if (item.type === ChatItemValueTypeEnum.file && !item.file?.url) {
return false;
}
return true;
}) || []
);
};
export const getPluginOutputsFromChatResponses = (responses: ChatHistoryItemResType[]) => {
const outputs =
responses.find((item) => item.moduleType === FlowNodeTypeEnum.pluginOutput)?.pluginOutput ?? {};
return outputs;
};

View File

@@ -142,6 +142,9 @@ export type DispatchNodeResponseType = {
// code
codeLog?: string;
// plugin
pluginOutput?: Record<string, any>;
};
export type DispatchNodeResultType<T> = {

View File

@@ -123,6 +123,7 @@ export const checkNodeRunStatus = ({
(item) => item.target === node.nodeId
);
// Entry
if (workflowEdges.length === 0) {
return 'run';
}

View File

@@ -27,7 +27,7 @@ export const ToolModule: FlowNodeTemplateType = {
sourceHandle: getHandleConfig(true, true, false, true),
targetHandle: getHandleConfig(true, true, false, true),
avatar: '/imgs/workflow/tool.svg',
name: '工具调用(实验)',
name: '工具调用',
intro: '通过AI模型自动选择一个或多个功能块进行调用也可以对插件进行调用。',
showStatus: true,
version: '481',

View File

@@ -22,6 +22,7 @@ import {
defaultWhisperConfig
} from '../app/constants';
import { IfElseResultEnum } from './template/system/ifElse/constant';
import { RuntimeNodeItemType } from './runtime/type';
export const getHandleId = (nodeId: string, type: 'source' | 'target', key: string) => {
return `${nodeId}-${type}-${key}`;
@@ -190,3 +191,38 @@ export const isReferenceValue = (value: any): boolean => {
export const getElseIFLabel = (i: number) => {
return i === 0 ? IfElseResultEnum.IF : `${IfElseResultEnum.ELSE_IF} ${i}`;
};
// add value to plugin input node when run plugin
export const updatePluginInputByVariables = (
nodes: RuntimeNodeItemType[],
variables: Record<string, any>
) => {
return nodes.map((node) =>
node.flowNodeType === FlowNodeTypeEnum.pluginInput
? {
...node,
inputs: node.inputs.map((input) => {
const parseValue = (() => {
try {
if (
input.valueType === WorkflowIOValueTypeEnum.string ||
input.valueType === WorkflowIOValueTypeEnum.number ||
input.valueType === WorkflowIOValueTypeEnum.boolean
)
return variables[input.key];
return JSON.parse(variables[input.key]);
} catch (e) {
return variables[input.key];
}
})();
return {
...input,
value: parseValue ?? input.value
};
})
}
: node
);
};

View File

@@ -1,5 +1,4 @@
import { SystemPluginResponseType } from '../../type';
import { urlsFetch } from '../../../service/common/string/cheerio';
import { urlsFetch } from '@fastgpt/service/common/string/cheerio';
type Props = {
url: string;

View File

@@ -58,6 +58,7 @@ const AppSchema = new Schema({
type: String,
default: ''
},
updateTime: {
type: Date,
default: () => new Date()
@@ -112,7 +113,7 @@ const AppSchema = new Schema({
...getPermissionSchema(AppDefaultPermissionVal)
});
AppSchema.index({ updateTime: -1 });
AppSchema.index({ teamId: 1, updateTime: -1 });
AppSchema.index({ teamId: 1, type: 1 });
AppSchema.index({ scheduledTriggerConfig: 1, intervalNextTime: -1 });

View File

@@ -89,7 +89,7 @@ try {
get chat logs;
close custom feedback;
*/
ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 }, { background: true });
ChatItemSchema.index({ appId: 1, chatId: 1, dataId: 1 }, { background: true, unique: true });
// admin charts
ChatItemSchema.index({ time: -1, obj: 1 }, { background: true });
// timer, clear history

View File

@@ -85,7 +85,7 @@ try {
// get user history
ChatSchema.index({ tmbId: 1, appId: 1, top: -1, updateTime: -1 }, { background: true });
// delete by appid; clear history; init chat; update chat; auth chat; get chat;
ChatSchema.index({ appId: 1, chatId: 1 }, { background: true });
ChatSchema.index({ appId: 1, chatId: 1 }, { background: true, unique: true });
// get chat logs;
ChatSchema.index({ teamId: 1, appId: 1, updateTime: -1 }, { background: true });

View File

@@ -8,6 +8,7 @@ import {
DragStart,
DropResult
} from 'react-beautiful-dnd';
export * from 'react-beautiful-dnd';
type Props<T = any> = {
onDragEndCb: (result: T[]) => void;
@@ -57,5 +58,3 @@ function DndDrag<T>({ children, renderClone, onDragEndCb, dataList }: Props<T>)
}
export default DndDrag;
export * from 'react-beautiful-dnd';

View File

@@ -6,15 +6,15 @@ const CloseIcon = (props: FlexProps) => {
return (
<Flex
cursor={'pointer'}
w={'22px'}
h={'22px'}
w={'1.5rem'}
h={'1.5rem'}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'50%'}
_hover={{ bg: 'myGray.200' }}
{...props}
>
<MyIcon name={'common/closeLight'} w={'12px'} color={'myGray.500'} />
<MyIcon name={'common/closeLight'} w={'80%'} h={'80%'} color={'myGray.500'} />
</Flex>
);
};

View File

@@ -7,11 +7,11 @@ import {
ModalCloseButton,
ModalContentProps,
Box,
Image,
useMediaQuery
Image
} from '@chakra-ui/react';
import MyIcon from '../Icon';
import MyBox from '../MyBox';
import { useSystem } from '../../../hooks/useSystem';
export interface MyModalProps extends ModalContentProps {
iconSrc?: string;
@@ -34,7 +34,7 @@ const MyModal = ({
maxW = ['90vw', '600px'],
...props
}: MyModalProps) => {
const [isPc] = useMediaQuery('(min-width: 900px)');
const isPc = useSystem();
return (
<Modal

View File

@@ -81,7 +81,6 @@ const MultipleSelect = <T = any,>({
borderRadius={'md'}
border={'base'}
userSelect={'none'}
minH={'40px'}
cursor={'pointer'}
_active={{
transform: 'none'

View File

@@ -1,4 +1,11 @@
import React, { useRef, forwardRef, useMemo } from 'react';
import React, {
useRef,
forwardRef,
useMemo,
useEffect,
useImperativeHandle,
ForwardedRef
} from 'react';
import {
Menu,
MenuList,
@@ -28,17 +35,21 @@ export type SelectProps<T = any> = ButtonProps & {
onchange?: (val: T) => void;
};
const MySelect = <T = any,>({
placeholder,
value,
width = '100%',
list = [],
onchange,
isLoading = false,
...props
}: SelectProps<T>) => {
const ref = useRef<HTMLButtonElement>(null);
const { Loading } = useLoading();
const MySelect = <T = any,>(
{
placeholder,
value,
width = '100%',
list = [],
onchange,
isLoading = false,
...props
}: SelectProps<T>,
ref: ForwardedRef<{
focus: () => void;
}>
) => {
const ButtonRef = useRef<HTMLButtonElement>(null);
const menuItemStyles: MenuItemProps = {
borderRadius: 'sm',
py: 2,
@@ -54,6 +65,12 @@ const MySelect = <T = any,>({
const { isOpen, onOpen, onClose } = useDisclosure();
const selectItem = useMemo(() => list.find((item) => item.value === value), [list, value]);
useImperativeHandle(ref, () => ({
focus() {
onOpen();
}
}));
return (
<Box
css={css({
@@ -72,7 +89,7 @@ const MySelect = <T = any,>({
>
<MenuButton
as={Button}
ref={ref}
ref={ButtonRef}
width={width}
px={3}
rightIcon={<ChevronDownIcon />}
@@ -98,7 +115,7 @@ const MySelect = <T = any,>({
<MenuList
className={props.className}
minW={(() => {
const w = ref.current?.clientWidth;
const w = ButtonRef.current?.clientWidth;
if (w) {
return `${w}px !important`;
}
@@ -152,4 +169,6 @@ const MySelect = <T = any,>({
);
};
export default MySelect;
export default forwardRef(MySelect) as <T>(
props: SelectProps<T> & { ref?: React.Ref<HTMLSelectElement> }
) => JSX.Element;

View File

@@ -1,14 +1,13 @@
import React from 'react';
import { Box, Tooltip, TooltipProps, css, useMediaQuery } from '@chakra-ui/react';
import { Box, Tooltip, TooltipProps } from '@chakra-ui/react';
import { useSystem } from '../../../hooks/useSystem';
interface Props extends TooltipProps {
forceShow?: boolean;
}
interface Props extends TooltipProps {}
const MyTooltip = ({ children, forceShow = false, shouldWrapChildren = true, ...props }: Props) => {
const [isPc] = useMediaQuery('(min-width: 900px)');
const MyTooltip = ({ children, shouldWrapChildren = true, ...props }: Props) => {
const { isPc } = useSystem();
return isPc || forceShow ? (
return (
<Tooltip
className="chakra-tooltip"
bg={'white'}
@@ -27,8 +26,6 @@ const MyTooltip = ({ children, forceShow = false, shouldWrapChildren = true, ...
>
{children}
</Tooltip>
) : (
<>{children}</>
);
};

View File

@@ -51,6 +51,7 @@ const LightRowTabs = <ValueType = string,>({
borderRadius={'sm'}
fontSize={sizeMap.fontSize}
overflowX={'auto'}
userSelect={'none'}
{...props}
>
{list.map((item) => (

View File

@@ -23,6 +23,8 @@ type Props = Omit<BoxProps, 'resize' | 'onChange'> & {
variables?: EditorVariablePickerType[];
defaultHeight?: number;
placeholder?: string;
isDisabled?: boolean;
isInvalid?: boolean;
};
const options = {
@@ -55,6 +57,8 @@ const JSONEditor = ({
variables = [],
placeholder,
defaultHeight = 100,
isDisabled = false,
isInvalid = false,
...props
}: Props) => {
const { toast } = useToast();
@@ -209,9 +213,9 @@ const JSONEditor = ({
return (
<Box
borderWidth={'1px'}
borderWidth={isInvalid ? '2px' : '1px'}
borderRadius={'md'}
borderColor={'myGray.200'}
borderColor={isInvalid ? 'red.500' : 'myGray.200'}
py={2}
height={height}
position={'relative'}

View File

@@ -26,6 +26,7 @@
"Export Configs": "Export Configs",
"Feedback Count": "User Feedback",
"Go to chat": "To chat",
"Go to run": "Run",
"Import Configs": "Import Configs",
"Import Configs Failed": "Failed to import configs, please ensure configs are valid!",
"Input Field Settings": "Input Field Settings",
@@ -39,8 +40,12 @@
"My Apps": "My Apps",
"Output Field Settings": "Output Field Settings",
"Paste Config": "Paste Config",
"Plugin dispatch": "Plugins",
"Plugin dispatch tip": "It is up to the model to decide which plug-ins to add additional capabilities to. If the plug-in is selected, the knowledge base call is also treated as a special plug-in.",
"Publish channel": "Publish channel",
"Publish success": "Publish success",
"Run": "Run",
"Search app": "Search app",
"Setting app": "Settings",
"Setting plugin": "Setting plugin",
"To Chat": "Go to Chat",

View File

@@ -107,8 +107,10 @@
"Rename Success": "Rename Success",
"Request Error": "Request Error",
"Require Input": "Required Input",
"Restart": "Restart",
"Role": "Role",
"Root folder": "Root folder",
"Run": "Run",
"Save": "Save",
"Save Failed": "Save Failed",
"Save Success": "Save Success",

View File

@@ -25,6 +25,7 @@
"Export Configs": "导出配置",
"Feedback Count": "用户反馈",
"Go to chat": "去对话",
"Go to run": "去运行",
"Import Configs": "导入配置",
"Import Configs Failed": "导入配置失败,请确保配置正常!",
"Input Field Settings": "输入字段编辑",
@@ -38,8 +39,12 @@
"My Apps": "我的应用",
"Output Field Settings": "输出字段编辑",
"Paste Config": "粘贴配置",
"Plugin dispatch": "插件调用",
"Plugin dispatch tip": "给模型附加额外的能力,具体调用哪些插件,将由模型自主决定。\n若选择了插件知识库调用将自动作为一个特殊的插件。",
"Publish channel": "发布渠道",
"Publish success": "发布成功",
"Run": "运行",
"Search app": "搜索应用",
"Setting app": "应用配置",
"Setting plugin": "插件配置",
"To Chat": "前去对话",

View File

@@ -108,8 +108,10 @@
"Rename Success": "重命名成功",
"Request Error": "请求异常",
"Require Input": "必填",
"Restart": "重新开始",
"Role": "权限",
"Root folder": "根目录",
"Run": "运行",
"Save": "保存",
"Save Failed": "保存失败",
"Save Success": "保存成功",

View File

@@ -38,8 +38,8 @@
"devDependencies": {
"@types/lodash": "^4.14.191",
"@types/papaparse": "^5.3.7",
"@types/react": "18.3.0",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react": "18.3.1",
"@types/react-beautiful-dnd": "^13.1.1",
"@types/react-dom": "18.3.0"
}
}

View File

@@ -314,7 +314,7 @@ const Input: ComponentStyleConfig = {
}),
md: defineStyle({
field: {
h: '40px',
h: '34px',
borderRadius: 'md'
}
})