Plugin support select file (#2756)

* feat: plugin support upload files (#2716)

* feat: plugin support file upload

* file history

* fix history & chattest

* chore: code

* plugin config icon & file preview padding

* perf: undefined fn

* fix: plugin file numbers & plugin template add config (#2743)

* perf: run plugin without human message (#2749)

* perf: run plugin without human message

* fix build

* fix build

* rename node

* perf: ui

* perf: plugin rerun with last params & plugin log add file (#2755)

* perf: plugin run with last params & plugin log add file

* delete console

* perf: plugin refresh code

* fix: ts

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2024-09-20 14:12:58 +08:00
committed by shilin66
parent e8967ddd39
commit 91ce4b0166
59 changed files with 2032 additions and 854 deletions

View File

@@ -4,7 +4,7 @@ import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/cons
import { responseWrite } from '@fastgpt/service/common/response';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import type { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import type { UserChatItemType } from '@fastgpt/global/core/chat/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
@@ -13,7 +13,10 @@ import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { removeEmptyUserInput } from '@fastgpt/global/core/chat/utils';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { updatePluginInputByVariables } from '@fastgpt/global/core/workflow/utils';
import {
getPluginRunUserQuery,
updatePluginInputByVariables
} from '@fastgpt/global/core/workflow/utils';
import { NextAPI } from '@/service/middleware/entry';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
@@ -28,6 +31,7 @@ import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatch/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
export type Props = {
messages: ChatCompletionMessageParam[];
@@ -65,8 +69,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
throw new Error('Edges is not array');
}
const chatMessages = GPTMessages2Chats(messages);
const userInput = chatMessages.pop()?.value as UserChatItemValueItemType[] | undefined;
// console.log(JSON.stringify(chatMessages, null, 2), '====', chatMessages.length);
/* user auth */
@@ -82,6 +84,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const isPlugin = app.type === AppTypeEnum.plugin;
const userQuestion: UserChatItemType = (() => {
if (isPlugin) {
return getPluginRunUserQuery({
pluginInputs: getPluginInputsFromStoreNodes(app.modules),
variables,
files: variables.files
});
}
const latestHumanChat = chatMessages.pop() as UserChatItemType | undefined;
if (!latestHumanChat) {
throw new Error('User question is empty');
}
return latestHumanChat;
})();
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, chatMessages));
// Plugin need to replace inputs
@@ -89,7 +107,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables);
variables = {};
} else {
if (!userInput) {
if (!userQuestion.value) {
throw new Error('Params Error');
}
}
@@ -117,7 +135,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
runtimeNodes,
runtimeEdges: initWorkflowEdgeStatus(edges, chatMessages),
variables,
query: removeEmptyUserInput(userInput),
query: removeEmptyUserInput(userQuestion.value),
chatConfig,
histories: chatMessages,
stream: true,

View File

@@ -49,13 +49,16 @@ import { NextAPI } from '@/service/middleware/entry';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { updatePluginInputByVariables } from '@fastgpt/global/core/workflow/utils';
import {
getPluginRunUserQuery,
updatePluginInputByVariables
} from '@fastgpt/global/core/workflow/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
import { rewriteNodeOutputByHistories } from '@fastgpt/global/core/workflow/runtime/utils';
import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatch/utils';
import { getPluginRunUserQuery } from '@fastgpt/service/core/workflow/utils';
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
type FastGptWebChatProps = {
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
@@ -185,8 +188,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// Get obj=Human history
const userQuestion: UserChatItemType = (() => {
if (isPlugin) {
// TODOget plugin files from variables
return getPluginRunUserQuery({ nodes: app.modules, variables });
return getPluginRunUserQuery({
pluginInputs: getPluginInputsFromStoreNodes(app.modules),
variables,
files: variables.files
});
}
const latestHumanChat = chatMessages.pop() as UserChatItemType | undefined;

View File

@@ -145,6 +145,7 @@ const DetailLogsModal = ({
{isPlugin ? (
<Box px={5} pt={2} h={'100%'}>
<PluginRunBox
chatConfig={chat?.app?.chatConfig}
pluginInputs={chat?.app.pluginInputs}
variablesForm={variablesForm}
histories={chatRecords}

View File

@@ -408,10 +408,11 @@ export const useWorkflow = () => {
/* node */
const handleRemoveNode = useMemoizedFn((change: NodeRemoveChange, node: Node) => {
if (node.data.forbidDelete) {
return toast({
toast({
status: 'warning',
title: t('common:core.workflow.Can not delete node')
});
return false;
}
// If the node has child nodes, remove the child nodes
@@ -438,6 +439,8 @@ export const useWorkflow = () => {
setEdges((state) =>
state.filter((edge) => edge.source !== change.id && edge.target !== change.id)
);
return true;
});
const handleSelectNode = useMemoizedFn((change: NodeSelectionChange) => {
// If the node is not selected and the Ctrl key is pressed, select the node
@@ -506,8 +509,9 @@ export const useWorkflow = () => {
for (const change of changes) {
if (change.type === 'remove') {
const node = nodes.find((n) => n.id === change.id);
if (node) {
handleRemoveNode(change, node);
// 如果删除失败,则不继续执行
if (node && !handleRemoveNode(change, node)) {
return;
}
} else if (change.type === 'select') {
handleSelectNode(change);

View File

@@ -13,6 +13,11 @@ import { getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { useCreation } from 'ahooks';
import ChatFunctionTip from '@/components/core/app/Tip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { WorkflowContext } from '../../../context';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import FileSelect from '@/components/core/app/FileSelect';
import { userFilesInput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
import MyIcon from '@fastgpt/web/components/common/Icon';
type ComponentProps = {
chatConfig: AppChatConfigType;
@@ -60,6 +65,9 @@ const NodePluginConfig = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
>
<Container w={'360px'}>
<Instruction {...componentsProps} />
<Box pt={4}>
<FileSelectConfig {...componentsProps} />
</Box>
</Container>
</NodeCard>
);
@@ -72,6 +80,7 @@ function Instruction({ chatConfig: { instruction }, setAppDetail }: ComponentPro
return (
<>
<Flex>
<MyIcon name={'core/app/simpleMode/chat'} mr={2} w={'20px'} />
<FormLabel color={'myGray.600'} fontWeight={'medium'} fontSize={'14px'}>
{t('workflow:plugin.Instructions')}
</FormLabel>
@@ -100,3 +109,48 @@ function Instruction({ chatConfig: { instruction }, setAppDetail }: ComponentPro
</>
);
}
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
const pluginInputNode = nodes.find((item) => item.type === FlowNodeTypeEnum.pluginInput)!;
return (
<FileSelect
value={fileSelectConfig}
color={'myGray.600'}
fontWeight={'medium'}
fontSize={'14px'}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
fileSelectConfig: e
}
}));
// Dynamic add or delete userFilesInput
const canUploadFiles = e.canSelectFile || e.canSelectImg;
const repeatKey = pluginInputNode?.data.outputs.find(
(item) => item.key === userFilesInput.key
);
if (canUploadFiles) {
!repeatKey &&
onChangeNode({
nodeId: pluginInputNode.id,
type: 'addOutput',
value: userFilesInput
});
} else {
repeatKey &&
onChangeNode({
nodeId: pluginInputNode.id,
type: 'delOutput',
key: userFilesInput.key
});
}
}}
/>
);
}

View File

@@ -22,6 +22,7 @@ import { WorkflowContext } from '../../../context';
import IOTitle from '../../components/IOTitle';
import dynamic from 'next/dynamic';
import { defaultInput } from './InputEditModal';
import RenderOutput from '../render/RenderOutput';
const FieldEditModal = dynamic(() => import('./InputEditModal'));
@@ -140,9 +141,15 @@ const NodePluginInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
}}
/>
</Container>
{!!outputs.length && (
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
)}
</NodeCard>
);
}, [data, inputs, nodeId, onChangeNode, selected, t]);
}, [data, inputs, nodeId, onChangeNode, outputs, selected, t]);
return (
<>

View File

@@ -72,7 +72,7 @@ export const useChatTest = ({
const CustomChatContainer = useMemoizedFn(() =>
appDetail.type === AppTypeEnum.plugin ? (
<Box p={3}>
<Box p={5}>
<PluginRunBox
pluginInputs={pluginInputs}
variablesForm={variablesForm}

View File

@@ -17,7 +17,6 @@ import { useTranslation } from 'next-i18next';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import AIModelSelector from '@/components/Select/AIModelSelector';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import ComplianceTip from '@/components/common/ComplianceTip/index';