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

@@ -14,6 +14,7 @@ import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constan
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
export type ListAppBody = {
parentId?: ParentIdType;
@@ -55,8 +56,8 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
const searchMatch = searchKey
? {
$or: [
{ name: { $regex: searchKey, $options: 'i' } },
{ intro: { $regex: searchKey, $options: 'i' } }
{ name: { $regex: new RegExp(`${replaceRegChars(searchKey)}`, 'i') } },
{ intro: { $regex: new RegExp(`${replaceRegChars(searchKey)}`, 'i') } }
]
}
: {};
@@ -65,7 +66,14 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
return {
// get all chat app
teamId,
type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple] },
type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple, AppTypeEnum.plugin] },
...searchMatch
};
}
if (searchKey) {
return {
teamId,
...searchMatch
};
}
@@ -74,8 +82,7 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
teamId,
...(type && Array.isArray(type) && { type: { $in: type } }),
...(type && { type }),
...parseParentIdInMongo(parentId),
...searchMatch
...parseParentIdInMongo(parentId)
};
})();

View File

@@ -1,15 +1,10 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase } from '@/service/mongo';
import { sseErrRes } from '@fastgpt/service/common/response';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
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 {
ChatItemType,
ChatItemValueItemType,
UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type';
import type { UserChatItemValueItemType } 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';
@@ -18,10 +13,14 @@ import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
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 { NextAPI } from '@/service/middleware/entry';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
export type Props = {
history: ChatItemType[];
prompt: UserChatItemValueItemType[];
messages: ChatCompletionMessageParam[];
nodes: RuntimeNodeItemType[];
edges: RuntimeEdgeItemType[];
variables: Record<string, any>;
@@ -29,7 +28,7 @@ export type Props = {
appName: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: NextApiRequest, res: NextApiResponse) {
res.on('close', () => {
res.end();
});
@@ -38,26 +37,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.end();
});
let {
nodes = [],
edges = [],
history = [],
prompt,
variables = {},
appName,
appId
} = req.body as Props;
let { nodes = [], edges = [], messages = [], variables = {}, appName, appId } = req.body as Props;
try {
await connectToDatabase();
if (!history || !nodes || !prompt || prompt.length === 0) {
throw new Error('Prams Error');
}
if (!Array.isArray(nodes)) {
throw new Error('Nodes is not array');
}
if (!Array.isArray(edges)) {
throw new Error('Edges is not array');
}
// [histories, user]
const chatMessages = GPTMessages2Chats(messages);
const userInput = chatMessages.pop()?.value as UserChatItemValueItemType[] | undefined;
/* user auth */
const [{ app }, { teamId, tmbId }] = await Promise.all([
@@ -67,6 +51,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
authToken: true
})
]);
const isPlugin = app.type === AppTypeEnum.plugin;
if (!Array.isArray(nodes)) {
throw new Error('Nodes is not array');
}
if (!Array.isArray(edges)) {
throw new Error('Edges is not array');
}
// Plugin need to replace inputs
if (isPlugin) {
nodes = updatePluginInputByVariables(nodes, variables);
} else {
if (!userInput) {
throw new Error('Params Error');
}
}
// auth balance
const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId);
@@ -82,8 +83,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
runtimeNodes: nodes,
runtimeEdges: edges,
variables,
query: removeEmptyUserInput(prompt),
histories: history,
query: removeEmptyUserInput(userInput),
histories: chatMessages,
stream: true,
detail: true,
maxRunTimes: 200
@@ -117,6 +118,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
export default NextAPI(handler);
export const config = {
api: {
bodyParser: {

View File

@@ -11,6 +11,7 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
async function handler(
req: NextApiRequest,
@@ -53,6 +54,8 @@ async function handler(
}),
getAppLatestVersion(app._id, app)
]);
const pluginInputs =
app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs ?? [];
return {
chatId,
@@ -72,7 +75,9 @@ async function handler(
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro
intro: app.intro,
type: app.type,
pluginInputs
}
};
}

View File

@@ -15,6 +15,8 @@ import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -47,7 +49,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
chatId,
limit: 30,
field: `dataId obj value userGoodFeedback userBadFeedback ${
shareChat.responseDetail
shareChat.responseDetail || app.type === AppTypeEnum.plugin
? `adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
: ''
} `
@@ -56,11 +58,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
]);
// pick share response field
histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
}
});
app.type !== AppTypeEnum.plugin &&
histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
}
});
jsonRes<InitChatResponse>(res, {
data: {
@@ -82,7 +85,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro
intro: app.intro,
type: app.type,
pluginInputs:
app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)
?.inputs ?? []
}
}
});

View File

@@ -15,6 +15,8 @@ import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@@ -58,11 +60,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
]);
// pick share response field
histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
}
});
app.type !== AppTypeEnum.plugin &&
histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({ flowResponses: item.responseData });
}
});
jsonRes<InitChatResponse>(res, {
data: {
@@ -83,7 +86,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
chatModels: getChatModelNameListByModules(nodes),
name: app.name,
avatar: app.avatar,
intro: app.intro
intro: app.intro,
type: app.type,
pluginInputs:
app?.modules?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)
?.inputs ?? []
}
}
});

View File

@@ -3,7 +3,11 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { sseErrRes, jsonRes } from '@fastgpt/service/common/response';
import { addLog } from '@fastgpt/service/common/system/log';
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import {
ChatItemValueTypeEnum,
ChatRoleEnum,
ChatSourceEnum
} from '@fastgpt/global/core/chat/constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import type { ChatCompletionCreateParams } from '@fastgpt/global/core/ai/type.d';
@@ -28,10 +32,10 @@ import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
import {
concatHistories,
filterPublicNodeResponseData,
getChatTitleFromChatMessage,
removeEmptyUserInput
} from '@fastgpt/global/core/chat/utils';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
import { connectToDatabase } from '@/service/mongo';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema';
@@ -49,9 +53,17 @@ import { setEntryEntries } from '@fastgpt/service/core/workflow/dispatchV1/utils
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 { getNanoid } from '@fastgpt/global/common/string/tools';
import {
getPluginInputsFromStoreNodes,
getPluginRunContent
} from '@fastgpt/global/core/app/plugin/utils';
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
appId?: string;
};
@@ -59,14 +71,11 @@ export type Props = ChatCompletionCreateParams &
FastGptWebChatProps &
OutLinkChatAuthProps & {
messages: ChatCompletionMessageParam[];
responseChatItemId?: string;
stream?: boolean;
detail?: boolean;
variables: Record<string, any>;
variables: Record<string, any>; // Global variables or plugin inputs
};
export type ChatResponseType = {
newChatId: string;
quoteLen?: number;
};
type AuthResponseType = {
teamId: string;
@@ -89,7 +98,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
res.end();
});
const {
let {
chatId,
appId,
// share chat
@@ -98,41 +107,39 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// team chat
teamId: spaceTeamId,
teamToken,
stream = false,
detail = false,
messages = [],
variables = {}
variables = {},
responseChatItemId = getNanoid()
} = req.body as Props;
try {
const originIp = requestIp.getClientIp(req);
await connectToDatabase();
// body data check
if (!messages) {
throw new Error('Prams Error');
}
const originIp = requestIp.getClientIp(req);
const startTime = Date.now();
try {
if (!Array.isArray(messages)) {
throw new Error('messages is not array');
}
if (messages.length === 0) {
throw new Error('messages is empty');
}
let startTime = Date.now();
// Web chat params: [Human, AI]
/*
Web params: chatId + [Human]
API params: chatId + [Human]
API params: [histories, Human]
*/
const chatMessages = GPTMessages2Chats(messages);
if (chatMessages[chatMessages.length - 1].obj !== ChatRoleEnum.Human) {
chatMessages.pop();
}
// user question
const question = chatMessages.pop() as UserChatItemType;
if (!question) {
throw new Error('Question is empty');
}
// Computed start hook params
const startHookText = (() => {
// Chat
const userQuestion = chatMessages[chatMessages.length - 1] as UserChatItemType | undefined;
if (userQuestion) return chatValue2RuntimePrompt(userQuestion.value).text;
const { text, files } = chatValue2RuntimePrompt(question.value);
// plugin
return JSON.stringify(variables);
})();
/*
1. auth app permission
@@ -149,7 +156,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
outLinkUid,
chatId,
ip: originIp,
question: text
question: startHookText
});
}
// team space chat
@@ -169,8 +176,45 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
chatId
});
})();
const isPlugin = app.type === AppTypeEnum.plugin;
// 1. get and concat history; 2. get app workflow
// Check message type
if (isPlugin) {
detail = true;
} else {
if (messages.length === 0) {
throw new Error('messages is empty');
}
}
// Get obj=Human history
const userQuestion: UserChatItemType = (() => {
if (isPlugin) {
return {
dataId: getNanoid(24),
obj: ChatRoleEnum.Human,
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: getPluginRunContent({
pluginInputs: getPluginInputsFromStoreNodes(app.modules)
})
}
}
]
};
}
const latestHumanChat = chatMessages.pop() as UserChatItemType | undefined;
if (!latestHumanChat) {
throw new Error('User question is empty');
}
return latestHumanChat;
})();
const { text, files } = chatValue2RuntimePrompt(userQuestion.value);
// Get and concat history;
const limit = getMaxHistoryLimitFromNodes(app.modules);
const [{ histories }, { nodes, edges, chatConfig }] = await Promise.all([
getChatItems({
@@ -182,7 +226,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
getAppLatestVersion(app._id, app)
]);
const newHistories = concatHistories(histories, chatMessages);
const responseChatItemId: string | undefined = messages[messages.length - 1].dataId;
// Get runtimeNodes
const runtimeNodes = isPlugin
? updatePluginInputByVariables(
storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
variables
)
: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes));
/* start flow controller */
const { flowResponses, flowUsages, assistantResponses, newVariables } = await (async () => {
@@ -196,10 +247,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
app,
chatId,
responseChatItemId,
runtimeNodes: storeNodes2RuntimeNodes(nodes, getDefaultEntryNodeIds(nodes)),
runtimeNodes,
runtimeEdges: initWorkflowEdgeStatus(edges),
variables,
query: removeEmptyUserInput(question.value),
query: removeEmptyUserInput(userQuestion.value),
histories: newHistories,
stream,
detail,
@@ -245,6 +296,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return ChatSourceEnum.online;
})();
const newTitle = isPlugin
? variables.cTime ?? getSystemTime(user.timezone)
: getChatTitleFromChatMessage(userQuestion);
await saveChat({
chatId,
appId: app._id,
@@ -254,11 +309,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
appChatConfig: chatConfig,
variables: newVariables,
isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time
newTitle,
shareId,
outLinkUid: outLinkUserId,
source,
content: [
question,
userQuestion,
{
dataId: responseChatItemId,
obj: ChatRoleEnum.AI,
@@ -295,7 +351,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
});
if (detail) {
if (responseDetail) {
if (responseDetail || isPlugin) {
responseWrite({
res,
event: SseResponseEventEnum.flowResponses,
@@ -312,6 +368,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return assistantResponses[0].text?.content;
return assistantResponses;
})();
res.json({
...(detail ? { responseData: feResponseData, newVariables } : {}),
id: chatId || '',