This commit is contained in:
Archer
2023-10-07 18:02:20 +08:00
committed by GitHub
parent c65a36d3ab
commit 98ce5103a0
56 changed files with 868 additions and 282 deletions

View File

@@ -3,71 +3,39 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { CreateQuestionGuideProps } from '@/api/core/ai/agent/type';
import { getAIChatApi } from '@fastgpt/core/aiApi/config';
import { Prompt_QuestionGuide } from '@/prompts/core/agent';
import { pushQuestionGuideBill } from '@/service/common/bill/push';
import { defaultQGModel } from '@/pages/api/system/getInitData';
import { createQuestionGuide } from '@fastgpt/core/ai/functions/createQuestionGuide';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { messages } = req.body as CreateQuestionGuideProps;
const { user } = await authUser({ req, authToken: true, authApiKey: true, authBalance: true });
const { user } = await authUser({
req,
authOutLink: true,
authToken: true,
authApiKey: true,
authBalance: true
});
if (!user) {
throw new Error('user not found');
}
const qgModel = global.qgModel || defaultQGModel;
const chatAPI = getAIChatApi(user.openaiAccount);
const { data } = await chatAPI.createChatCompletion({
model: qgModel.model,
temperature: 0,
max_tokens: 200,
messages: [
...messages,
{
role: 'user',
content: Prompt_QuestionGuide
}
],
stream: false
const { result, tokens } = await createQuestionGuide({
messages,
model: (global.qgModel || defaultQGModel).model
});
const answer = data.choices?.[0].message?.content || '';
const totalTokens = data.usage?.total_tokens || 0;
jsonRes(res, {
data: result
});
const start = answer.indexOf('[');
const end = answer.lastIndexOf(']');
if (start === -1 || end === -1) {
return jsonRes(res, {
data: []
});
}
const jsonStr = answer
.substring(start, end + 1)
.replace(/(\\n|\\)/g, '')
.replace(/ /g, '');
try {
jsonRes(res, {
data: JSON.parse(jsonStr)
});
pushQuestionGuideBill({
tokens: totalTokens,
userId: user._id
});
return;
} catch (error) {
return jsonRes(res, {
data: []
});
}
pushQuestionGuideBill({
tokens: tokens,
userId: user._id
});
} catch (err) {
jsonRes(res, {
code: 500,

View File

@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authBalanceByUid, authUser } from '@/service/utils/auth';
import { withNextCors } from '@/service/utils/tools';
import { getAIChatApi, axiosConfig } from '@fastgpt/core/aiApi/config';
import { getAIChatApi, axiosConfig } from '@fastgpt/core/ai/config';
import { pushGenerateVectorBill } from '@/service/common/bill/push';
type Props = {

View File

@@ -5,7 +5,7 @@ import { User } from '@/service/models/user';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { UserUpdateParams } from '@/types/user';
import { axiosConfig, getAIChatApi, openaiBaseUrl } from '@fastgpt/core/aiApi/config';
import { axiosConfig, getAIChatApi, openaiBaseUrl } from '@fastgpt/core/ai/config';
/* update user info */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {

View File

@@ -12,9 +12,10 @@ import {
dispatchAnswer,
dispatchClassifyQuestion,
dispatchContentExtract,
dispatchHttpRequest
dispatchHttpRequest,
dispatchAppRequest
} from '@/service/moduleDispatch';
import type { CreateChatCompletionRequest } from '@fastgpt/core/aiApi/type';
import type { CreateChatCompletionRequest } from '@fastgpt/core/ai/type';
import type { MessageItemType } from '@/types/core/chat/type';
import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt';
import { getChatHistory } from './getHistory';
@@ -325,14 +326,19 @@ export async function dispatchModules({
responseData
}: {
answerText?: string;
responseData?: ChatHistoryItemResType;
responseData?: ChatHistoryItemResType | ChatHistoryItemResType[];
}) {
const time = Date.now();
responseData &&
chatResponse.push({
...responseData,
runningTime: +((time - runningTime) / 1000).toFixed(2)
});
if (responseData) {
if (Array.isArray(responseData)) {
chatResponse = chatResponse.concat(responseData);
} else {
chatResponse.push({
...responseData,
runningTime: +((time - runningTime) / 1000).toFixed(2)
});
}
}
runningTime = time;
chatAnswerText += answerText;
}
@@ -411,7 +417,7 @@ export async function dispatchModules({
variables,
moduleName: module.name,
outputs: module.outputs,
userOpenaiAccount: user?.openaiAccount,
user,
inputs: params
};
@@ -424,7 +430,8 @@ export async function dispatchModules({
[FlowModuleTypeEnum.kbSearchNode]: dispatchKBSearch,
[FlowModuleTypeEnum.classifyQuestion]: dispatchClassifyQuestion,
[FlowModuleTypeEnum.contentExtract]: dispatchContentExtract,
[FlowModuleTypeEnum.httpRequest]: dispatchHttpRequest
[FlowModuleTypeEnum.httpRequest]: dispatchHttpRequest,
[FlowModuleTypeEnum.app]: dispatchAppRequest
};
if (callbackMap[module.flowType]) {
return callbackMap[module.flowType](props);

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../modules/NodeCard';
import { FlowModuleItemType } from '@/types/core/app/flow';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
const NodeAPP = ({ data }: NodeProps<FlowModuleItemType>) => {
const { moduleId, inputs, outputs } = data;
return (
<NodeCard minW={'350px'} {...data}>
<Container borderTop={'2px solid'} borderTopColor={'myGray.200'}>
<RenderInput moduleId={moduleId} flowInputList={inputs} />
</Container>
<Divider text="Output" />
<Container>
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
</Container>
</NodeCard>
);
};
export default React.memo(NodeAPP);

View File

@@ -35,6 +35,7 @@ const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
export type useFlowStoreType = {
appId: string;
reactFlowWrapper: null | React.RefObject<HTMLDivElement>;
nodes: Node<FlowModuleItemType, string | undefined>[];
setNodes: Dispatch<SetStateAction<Node<FlowModuleItemType, string | undefined>[]>>;
@@ -58,6 +59,7 @@ export type useFlowStoreType = {
};
const StateContext = createContext<useFlowStoreType>({
appId: '',
reactFlowWrapper: null,
nodes: [],
setNodes: function (
@@ -109,7 +111,7 @@ const StateContext = createContext<useFlowStoreType>({
});
export const useFlowStore = () => useContext(StateContext);
export const FlowProvider = ({ children }: { children: React.ReactNode }) => {
export const FlowProvider = ({ appId, children }: { appId: string; children: React.ReactNode }) => {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const { toast } = useToast();
@@ -209,7 +211,6 @@ export const FlowProvider = ({ children }: { children: React.ReactNode }) => {
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
console.log(template);
setNodes((state) =>
state.concat(
appModule2FlowNode({
@@ -328,6 +329,7 @@ export const FlowProvider = ({ children }: { children: React.ReactNode }) => {
);
const value = {
appId,
reactFlowWrapper,
nodes,
setNodes,

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import type { FlowInputItemType } from '@/types/core/app/flow';
import type { FlowInputItemType, SelectAppItemType } from '@/types/core/app/flow';
import {
Box,
Textarea,
@@ -9,7 +9,10 @@ import {
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Flex
Flex,
useDisclosure,
Button,
useTheme
} from '@chakra-ui/react';
import { FlowInputItemTypeEnum } from '@/constants/flow';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
@@ -20,7 +23,9 @@ import MyTooltip from '@/components/MyTooltip';
import TargetHandle from './TargetHandle';
import MyIcon from '@/components/Icon';
const SetInputFieldModal = dynamic(() => import('../modules/SetInputFieldModal'));
const SelectAppModal = dynamic(() => import('../../../SelectAppModal'));
import { useFlowStore } from '../Provider';
import Avatar from '@/components/Avatar';
export const Label = ({
moduleId,
@@ -141,6 +146,7 @@ const RenderInput = ({
CustomComponent?: Record<string, (e: FlowInputItemType) => React.ReactNode>;
}) => {
const { onChangeNode } = useFlowStore();
return (
<>
{flowInputList.map(
@@ -253,6 +259,9 @@ const RenderInput = ({
{item.type === FlowInputItemTypeEnum.custom && CustomComponent[item.key] && (
<>{CustomComponent[item.key]({ ...item })}</>
)}
{item.type === FlowInputItemTypeEnum.selectApp && (
<RenderSelectApp app={item} moduleId={moduleId} />
)}
</Box>
</Box>
)
@@ -262,3 +271,54 @@ const RenderInput = ({
};
export default React.memo(RenderInput);
function RenderSelectApp({ app, moduleId }: { app: FlowInputItemType; moduleId: string }) {
const { onChangeNode, appId } = useFlowStore();
const theme = useTheme();
const {
isOpen: isOpenSelectApp,
onOpen: onOpenSelectApp,
onClose: onCloseSelectApp
} = useDisclosure();
const value = app.value as SelectAppItemType | undefined;
return (
<>
<Box onClick={onOpenSelectApp}>
{!value ? (
<Button variant={'base'} w={'100%'}>
</Button>
) : (
<Flex alignItems={'center'} border={theme.borders.base} borderRadius={'md'} px={3} py={2}>
<Avatar src={value?.logo} />
<Box fontWeight={'bold'} ml={1}>
{value?.name}
</Box>
</Flex>
)}
</Box>
{isOpenSelectApp && (
<SelectAppModal
defaultApps={app.value?.id ? [app.value.id] : []}
filterApps={[appId]}
onClose={onCloseSelectApp}
onSuccess={(e) => {
onChangeNode({
moduleId,
type: 'inputs',
key: 'app',
value: {
...app,
value: e[0]
}
});
}}
/>
)}
</>
);
}

View File

@@ -38,6 +38,7 @@ const NodeVariable = dynamic(() => import('./components/Nodes/NodeVariable'));
const NodeUserGuide = dynamic(() => import('./components/Nodes/NodeUserGuide'));
const NodeExtract = dynamic(() => import('./components/Nodes/NodeExtract'));
const NodeHttp = dynamic(() => import('./components/Nodes/NodeHttp'));
const NodeAPP = dynamic(() => import('./components/Nodes/NodeAPP'));
import 'reactflow/dist/style.css';
@@ -52,7 +53,8 @@ const nodeTypes = {
[FlowModuleTypeEnum.answerNode]: NodeAnswer,
[FlowModuleTypeEnum.classifyQuestion]: NodeCQNode,
[FlowModuleTypeEnum.contentExtract]: NodeExtract,
[FlowModuleTypeEnum.httpRequest]: NodeHttp
[FlowModuleTypeEnum.httpRequest]: NodeHttp,
[FlowModuleTypeEnum.app]: NodeAPP
// [FlowModuleTypeEnum.empty]: EmptyModule
};
const edgeTypes = {
@@ -116,8 +118,17 @@ function FlowHeader({ app, onCloseSettings }: Props & {}) {
const { mutate: onclickSave, isLoading } = useRequest({
mutationFn: () => {
const modules = flow2AppModules();
// check required connect
for (let i = 0; i < modules.length; i++) {
const item = modules[i];
if (item.inputs.find((input) => input.required && !input.connected)) {
return Promise.reject(`${item.name}】存在未连接的必填输入`);
}
}
return updateAppDetail(app._id, {
modules: flow2AppModules(),
modules: modules,
type: AppTypeEnum.advanced
});
},
@@ -314,7 +325,7 @@ const Flow = (data: Props) => {
return (
<Box h={'100%'} position={'fixed'} zIndex={999} top={0} left={0} right={0} bottom={0}>
<ReactFlowProvider>
<FlowProvider>
<FlowProvider appId={data?.app?._id}>
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
{!!data.app._id && <AppEdit {...data} />}
</Flex>

View File

@@ -69,7 +69,12 @@ export const DatasetSelectModal = ({
tips={'仅能选择同一个索引模型的知识库'}
onClose={onClose}
>
<ModalBody flex={['1 0 0', '0 0 auto']} maxH={'80vh'} overflowY={'auto'} userSelect={'none'}>
<ModalBody
flex={['1 0 0', '1 0 auto']}
maxH={'80vh'}
overflowY={['auto', 'unset']}
userSelect={'none'}
>
<Grid gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']} gridGap={3}>
{filterKbList.selected.map((item) =>
(() => {

View File

@@ -287,7 +287,6 @@ function DetailLogsModal({
<Box pt={2} flex={'1 0 0'}>
<ChatBox
ref={ChatBoxRef}
chatId={chatId}
appAvatar={chat?.app.avatar}
userAvatar={HUMAN_ICON}
feedbackType={'admin'}

View File

@@ -0,0 +1,110 @@
import React, { useMemo } from 'react';
import { ModalBody, Flex, Box, useTheme, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { getMyModels } from '@/api/app';
import { useQuery } from '@tanstack/react-query';
import type { SelectAppItemType } from '@/types/core/app/flow';
import Avatar from '@/components/Avatar';
import { useTranslation } from 'react-i18next';
import { useLoading } from '@/hooks/useLoading';
const SelectAppModal = ({
defaultApps = [],
filterApps = [],
max = 1,
onClose,
onSuccess
}: {
defaultApps: string[];
filterApps?: string[];
max?: number;
onClose: () => void;
onSuccess: (e: SelectAppItemType[]) => void;
}) => {
const { t } = useTranslation();
const { Loading } = useLoading();
const theme = useTheme();
const [selectedApps, setSelectedApps] = React.useState<string[]>(defaultApps);
/* 加载模型 */
const { data = [], isLoading } = useQuery(['loadMyApos'], () => getMyModels());
const apps = useMemo(
() => data.filter((app) => !filterApps.includes(app._id)),
[data, filterApps]
);
return (
<MyModal
isOpen
title={`选择应用${max > 1 ? `(${selectedApps.length}/${max})` : ''}`}
onClose={onClose}
w={'700px'}
position={'relative'}
>
<ModalBody
minH={'300px'}
display={'grid'}
gridTemplateColumns={['1fr', 'repeat(3,1fr)']}
gridGap={4}
>
{apps.map((app) => (
<Flex
key={app._id}
alignItems={'center'}
border={theme.borders.base}
borderRadius={'md'}
px={1}
py={2}
cursor={'pointer'}
{...(selectedApps.includes(app._id)
? {
bg: 'myBlue.200',
onClick: () => {
setSelectedApps(selectedApps.filter((e) => e !== app._id));
}
}
: {
onClick: () => {
if (max === 1) {
setSelectedApps([app._id]);
} else if (selectedApps.length < max) {
setSelectedApps([...selectedApps, app._id]);
}
}
})}
>
<Avatar src={app.avatar} w={['16px', '22px']} />
<Box fontWeight={'bold'} ml={1}>
{app.name}
</Box>
</Flex>
))}
</ModalBody>
<ModalFooter>
<Button variant={'base'} onClick={onClose}>
{t('Cancel')}
</Button>
<Button
ml={2}
onClick={() => {
onSuccess(
apps
.filter((app) => selectedApps.includes(app._id))
.map((app) => ({
id: app._id,
name: app.name,
logo: app.avatar
}))
);
onClose();
}}
>
{t('Confirm')}
</Button>
</ModalFooter>
<Loading loading={isLoading} fixed={false} />
</MyModal>
);
};
export default React.memo(SelectAppModal);

View File

@@ -64,7 +64,7 @@ const MyApps = () => {
);
/* 加载模型 */
useQuery(['loadModels'], () => loadMyApps(true), {
useQuery(['loadApps'], () => loadMyApps(true), {
refetchOnMount: true
});

View File

@@ -8,7 +8,6 @@ import { useRouter } from 'next/router';
const ToolMenu = ({ history }: { history: ChatItemType[] }) => {
const { onExportChat } = useChatBox();
const router = useRouter();
const { appId, shareId } = router.query;
const menuList = useMemo(
() => [
@@ -18,8 +17,8 @@ const ToolMenu = ({ history }: { history: ChatItemType[] }) => {
onClick: () => {
router.replace({
query: {
appId,
shareId
...router.query,
chatId: ''
}
});
}
@@ -36,7 +35,7 @@ const ToolMenu = ({ history }: { history: ChatItemType[] }) => {
},
{ icon: 'pdf', label: 'PDF导出', onClick: () => onExportChat({ type: 'pdf', history }) }
],
[appId, history, onExportChat, router, shareId]
[history, onExportChat, router]
);
return history.length > 0 ? (

View File

@@ -358,7 +358,6 @@ const Chat = ({ appId, chatId }: { appId: string; chatId: string }) => {
<ChatBox
ref={ChatBoxRef}
showEmptyIntro
chatId={chatId}
appAvatar={chatData.app.avatar}
userAvatar={userInfo?.avatar}
userGuideModule={chatData.app?.userGuideModule}

View File

@@ -24,10 +24,12 @@ import { serviceSideProps } from '@/utils/web/i18n';
const OutLink = ({
shareId,
chatId,
showHistory,
authToken
}: {
shareId: string;
chatId: string;
showHistory: '0' | '1';
authToken?: string;
}) => {
const router = useRouter();
@@ -89,9 +91,8 @@ const OutLink = ({
forbidRefresh.current = true;
router.replace({
query: {
shareId,
chatId: completionChatId,
authToken
...router.query,
chatId: completionChatId
}
});
}
@@ -174,59 +175,58 @@ const OutLink = ({
<title>{shareChatData.app.name}</title>
</Head>
<Flex h={'100%'} flexDirection={['column', 'row']}>
{((children: React.ReactNode) => {
return isPc ? (
<SideBar>{children}</SideBar>
) : (
<Drawer
isOpen={isOpenSlider}
placement="left"
autoFocus={false}
size={'xs'}
onClose={onCloseSlider}
>
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxWidth={'250px'} boxShadow={'2px 0 10px rgba(0,0,0,0.15)'}>
{children}
</DrawerContent>
</Drawer>
);
})(
<ChatHistorySlider
appName={shareChatData.app.name}
appAvatar={shareChatData.app.avatar}
activeChatId={chatId}
history={history.map((item) => ({
id: item.chatId,
title: item.title
}))}
onClose={onCloseSlider}
onChangeChat={(chatId) => {
console.log(chatId);
router.replace({
query: {
chatId: chatId || '',
shareId,
authToken
}
});
if (!isPc) {
onCloseSlider();
}
}}
onDelHistory={delOneShareHistoryByChatId}
onClearHistory={() => {
delManyShareChatHistoryByShareId(shareId);
router.replace({
query: {
shareId,
authToken
}
});
}}
/>
)}
{showHistory === '1'
? ((children: React.ReactNode) => {
return isPc ? (
<SideBar>{children}</SideBar>
) : (
<Drawer
isOpen={isOpenSlider}
placement="left"
autoFocus={false}
size={'xs'}
onClose={onCloseSlider}
>
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxWidth={'250px'} boxShadow={'2px 0 10px rgba(0,0,0,0.15)'}>
{children}
</DrawerContent>
</Drawer>
);
})(
<ChatHistorySlider
appName={shareChatData.app.name}
appAvatar={shareChatData.app.avatar}
activeChatId={chatId}
history={history.map((item) => ({
id: item.chatId,
title: item.title
}))}
onClose={onCloseSlider}
onChangeChat={(chatId) => {
router.replace({
query: {
...router.query,
chatId: chatId || ''
}
});
if (!isPc) {
onCloseSlider();
}
}}
onDelHistory={delOneShareHistoryByChatId}
onClearHistory={() => {
delManyShareChatHistoryByShareId(shareId);
router.replace({
query: {
...router.query,
chatId: ''
}
});
}}
/>
)
: null}
{/* chat container */}
<Flex
@@ -276,10 +276,11 @@ const OutLink = ({
export async function getServerSideProps(context: any) {
const shareId = context?.query?.shareId || '';
const chatId = context?.query?.chatId || '';
const showHistory = context?.query?.showHistory || '1';
const authToken = context?.query?.authToken || '';
return {
props: { shareId, chatId, authToken, ...(await serviceSideProps(context)) }
props: { shareId, chatId, showHistory, authToken, ...(await serviceSideProps(context)) }
};
}