extract modules

This commit is contained in:
archer
2023-07-31 18:17:27 +08:00
parent 2f28f57d78
commit 58153306c5
17 changed files with 377 additions and 78 deletions

View File

@@ -18,6 +18,13 @@
"New Chat": "New Chat",
"You need to a chat app": "You need to a chat app"
},
"common": {
"Add": "Add",
"Filed is repeat": "Filed is repeated",
"Filed is repeated": "",
"Input": "Input",
"Output": "Output"
},
"home": {
"Quickly build AI question and answer library": "Quickly build AI question and answer library",
"Start Now": "Start Now",

View File

@@ -18,6 +18,13 @@
"New Chat": "新对话",
"You need to a chat app": "你需要创建一个应用"
},
"common": {
"Add": "添加",
"Filed is repeat": "",
"Filed is repeated": "字段重复了",
"Input": "输入",
"Output": "输出"
},
"home": {
"Quickly build AI question and answer library": "快速搭建 AI 问答系统",
"Start Now": "立即开始",

View File

@@ -54,7 +54,8 @@ export const ChatSourceMap = {
export enum ChatModuleEnum {
'AIChat' = 'AI Chat',
'KBSearch' = 'KB Search',
'CQ' = 'Classify Question'
'CQ' = 'Classify Question',
'Extract' = 'Content Extract'
}
export enum OutLinkTypeEnum {

View File

@@ -15,6 +15,7 @@ import {
Input_Template_TFSwitch,
Input_Template_UserChatInput
} from './inputTemplate';
import { ContextExtractEnum } from './flowField';
export const ChatModelSystemTip =
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。可使用变量,例如 {{language}}';
@@ -333,7 +334,7 @@ export const ClassifyQuestionModule: FlowModuleTemplateType = {
Input_Template_History,
Input_Template_UserChatInput,
{
key: 'agents',
key: SpecialInputKeyEnum.agents,
type: FlowInputItemTypeEnum.custom,
label: '',
value: [
@@ -375,59 +376,70 @@ export const ClassifyQuestionModule: FlowModuleTemplateType = {
};
export const ContextExtractModule: FlowModuleTemplateType = {
logo: '/imgs/module/extract.png',
name: '内容提取',
name: '文本内容提取',
intro: '从文本中提取出指定格式的数据',
description: '可从文本中提取指定的数据例如sql语句、搜索关键词、代码等',
flowType: FlowModuleTypeEnum.contentExtract,
inputs: [
Input_Template_TFSwitch,
{
key: 'systemPrompt',
key: ContextExtractEnum.description,
type: FlowInputItemTypeEnum.textarea,
valueType: FlowValueTypeEnum.string,
label: '提取内容描述',
label: '提取要求描述',
description: '写一段提取要求,告诉 AI 需要提取哪些内容',
placeholder: '例如: \n1. 根据用户的\n2. Sealos 是一个集群操作系统',
placeholder:
'例如: \n1. 你是一个实验室预约助手。根据用户问题,提取出姓名、实验室号和预约时间',
value: ''
},
Input_Template_History,
Input_Template_UserChatInput,
{
key: 'agents',
key: ContextExtractEnum.content,
type: FlowInputItemTypeEnum.target,
label: '需要提取的文本',
required: true,
valueType: FlowValueTypeEnum.string
},
{
key: ContextExtractEnum.extractKeys,
type: FlowInputItemTypeEnum.custom,
label: '',
label: '目标字段',
description: "由 '描述' 和 'key' 组成一个目标字段,可提取多个目标字段",
value: [
{
value: '打招呼',
key: 'fasw'
key: 'a',
desc: '描述',
required: true
},
{
value: '关于 xxx 的问题',
key: 'fqsw'
},
{
value: '其他问题',
key: 'fesw'
key: 'n',
desc: '描述',
required: true
}
]
}
],
outputs: [
{
key: 'fasw',
label: '',
type: FlowOutputItemTypeEnum.hidden,
key: ContextExtractEnum.success,
label: '字段完全提取',
valueType: FlowValueTypeEnum.boolean,
type: FlowOutputItemTypeEnum.source,
targets: []
},
{
key: 'fqsw',
label: '',
type: FlowOutputItemTypeEnum.hidden,
key: ContextExtractEnum.failed,
label: '提取字段缺失',
valueType: FlowValueTypeEnum.boolean,
type: FlowOutputItemTypeEnum.source,
targets: []
},
{
key: 'fesw',
label: '',
type: FlowOutputItemTypeEnum.hidden,
key: ContextExtractEnum.fields,
label: '提取结果',
description: '一个 JSON 对象,例如 {"name:":"YY","Time":"2023/7/2 18:00"}',
valueType: FlowValueTypeEnum.other,
type: FlowOutputItemTypeEnum.source,
targets: []
}
]
@@ -461,7 +473,7 @@ export const ModuleTemplates = [
},
{
label: 'Agent',
list: [ClassifyQuestionModule]
list: [ClassifyQuestionModule, ContextExtractModule]
}
];
export const ModuleTemplatesFlat = ModuleTemplates.map((templates) => templates.list)?.flat();
@@ -1070,7 +1082,7 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] =
connected: true
},
{
key: 'agents',
key: SpecialInputKeyEnum.agents,
value: [
{
value: '打招呼、问候等问题',

View File

@@ -0,0 +1,8 @@
export enum ContextExtractEnum {
extractKeys = 'extractKeys',
content = 'content',
description = 'description',
success = 'success',
failed = 'failed',
fields = 'fields'
}

View File

@@ -35,7 +35,8 @@ export enum FlowModuleTypeEnum {
}
export enum SpecialInputKeyEnum {
'answerText' = 'text'
'answerText' = 'text',
'agents' = 'agents' // cq agent key
}
export enum FlowValueTypeEnum {

View File

@@ -25,6 +25,7 @@ import { pushTaskBill } from '@/service/events/pushBill';
import { BillSourceEnum } from '@/constants/user';
import { ChatHistoryItemResType } from '@/types/chat';
import { UserModelSchema } from '@/types/mongoSchema';
import { dispatchContentExtract } from '@/service/moduleDispatch/agent/extract';
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
type FastGptWebChatProps = {
@@ -337,7 +338,8 @@ export async function dispatchModules({
[FlowModuleTypeEnum.answerNode]: dispatchAnswer,
[FlowModuleTypeEnum.chatNode]: dispatchChatCompletion,
[FlowModuleTypeEnum.kbSearchNode]: dispatchKBSearch,
[FlowModuleTypeEnum.classifyQuestion]: dispatchClassifyQuestion
[FlowModuleTypeEnum.classifyQuestion]: dispatchClassifyQuestion,
[FlowModuleTypeEnum.contentExtract]: dispatchContentExtract
};
if (callbackMap[module.flowType]) {
return callbackMap[module.flowType](props);

View File

@@ -10,7 +10,7 @@ import type { ClassifyQuestionAgentItemType } from '@/types/app';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 4);
import MyIcon from '@/components/Icon';
import { FlowOutputItemTypeEnum, FlowValueTypeEnum } from '@/constants/flow';
import { FlowOutputItemTypeEnum, FlowValueTypeEnum, SpecialInputKeyEnum } from '@/constants/flow';
import SourceHandle from '../render/SourceHandle';
const NodeCQNode = ({
@@ -25,7 +25,7 @@ const NodeCQNode = ({
onChangeNode={onChangeNode}
flowInputList={inputs}
CustomComponent={{
agents: ({
[SpecialInputKeyEnum.agents]: ({
key: agentKey,
value: agents = []
}: {

View File

@@ -0,0 +1,146 @@
import React, { useState } from 'react';
import { Box, Button, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react';
import { NodeProps } from 'reactflow';
import { FlowModuleItemType } from '@/types/flow';
import { useTranslation } from 'next-i18next';
import NodeCard from '../modules/NodeCard';
import Container from '../modules/Container';
import { AddIcon } from '@chakra-ui/icons';
import RenderInput from '../render/RenderInput';
import Divider from '../modules/Divider';
import { ContextExtractAgentItemType } from '@/types/app';
import RenderOutput from '../render/RenderOutput';
import MyIcon from '@/components/Icon';
import ExtractFieldModal from '../modules/ExtractFieldModal';
import { ContextExtractEnum } from '@/constants/flow/flowField';
const NodeExtract = ({
data: { inputs, outputs, moduleId, onChangeNode, ...props }
}: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const [editExtractFiled, setEditExtractField] = useState<ContextExtractAgentItemType>();
return (
<NodeCard minW={'380px'} moduleId={moduleId} {...props}>
<Divider text="Input" />
<Container>
<RenderInput
moduleId={moduleId}
onChangeNode={onChangeNode}
flowInputList={inputs}
CustomComponent={{
[ContextExtractEnum.extractKeys]: ({
key,
value: extractKeys = []
}: {
key: string;
value?: ContextExtractAgentItemType[];
}) => (
<Box>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th> key</Th>
<Th></Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{extractKeys.map((item, index) => (
<Tr key={index}>
<Td>{item.key}</Td>
<Td whiteSpace={'pre-line'} wordBreak={'break-all'}>
{item.desc}{' '}
</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td whiteSpace={'nowrap'}>
<MyIcon
mr={3}
name={'settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
setEditExtractField(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
onChangeNode({
moduleId,
type: 'inputs',
key: ContextExtractEnum.extractKeys,
value: extractKeys.filter((extract) => item.key !== extract.key)
});
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<Box mt={2} textAlign={'right'}>
<Button
variant={'base'}
leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() =>
setEditExtractField({
desc: '',
key: '',
required: true
})
}
>
</Button>
</Box>
</Box>
)
}}
/>
</Container>
<Divider text="Output" />
<Container>
<RenderOutput flowOutputList={outputs} />
</Container>
{!!editExtractFiled && (
<ExtractFieldModal
defaultField={editExtractFiled}
onClose={() => setEditExtractField(undefined)}
onSubmit={(data) => {
const extracts: ContextExtractAgentItemType[] =
inputs.find((item) => item.key === ContextExtractEnum.extractKeys)?.value || [];
const exists = extracts.find((item) => item.key === editExtractFiled.key);
if (exists) {
onChangeNode({
moduleId,
type: 'inputs',
key: ContextExtractEnum.extractKeys,
value: extracts.map((item) => (item.key === editExtractFiled.key ? data : item))
});
} else {
onChangeNode({
moduleId,
type: 'inputs',
key: ContextExtractEnum.extractKeys,
value: extracts.concat(data)
});
}
setEditExtractField(undefined);
}}
/>
)}
</NodeCard>
);
};
export default NodeExtract;

View File

@@ -1,8 +1,10 @@
import React from 'react';
import { Box, useTheme } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
const Divider = ({ text }: { text: 'Body' | 'Input' | 'Output' | string }) => {
const Divider = ({ text }: { text: 'Input' | 'Output' | string }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Box
textAlign={'center'}
@@ -12,7 +14,7 @@ const Divider = ({ text }: { text: 'Body' | 'Input' | 'Output' | string }) => {
borderBottom={theme.borders.base}
fontSize={'lg'}
>
{text}
{t(`common.${text}`)}
</Box>
);
};

View File

@@ -0,0 +1,74 @@
import React, { useState } from 'react';
import {
Box,
Button,
ModalHeader,
ModalFooter,
ModalBody,
Flex,
Switch,
Input,
FormControl
} from '@chakra-ui/react';
import type { ContextExtractAgentItemType } from '@/types/app';
import { useForm } from 'react-hook-form';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
import MyModal from '@/components/MyModal';
import Avatar from '@/components/Avatar';
const ExtractFieldModal = ({
defaultField = {
desc: '',
key: '',
required: true
},
onClose,
onSubmit
}: {
defaultField?: ContextExtractAgentItemType;
onClose: () => void;
onSubmit: (data: ContextExtractAgentItemType) => void;
}) => {
const { register, handleSubmit } = useForm<ContextExtractAgentItemType>({
defaultValues: defaultField
});
return (
<MyModal isOpen={true} onClose={onClose}>
<ModalHeader display={'flex'} alignItems={'center'}>
<Avatar src={'/imgs/module/extract.png'} mr={2} w={'20px'} objectFit={'cover'} />
</ModalHeader>
<ModalBody>
<Flex alignItems={'center'}>
<Box w={'70px'}></Box>
<Switch {...register('required')} />
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box w={'80px'}></Box>
<Input
placeholder="姓名/年龄/sql语句……"
{...register('desc', { required: '字段描述不能为空' })}
/>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box w={'80px'}> key</Box>
<Input
placeholder="name/age/sql"
{...register('key', { required: '字段 key 不能为空' })}
/>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button onClick={handleSubmit(onSubmit)}></Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ExtractFieldModal);

View File

@@ -29,7 +29,14 @@ const NodeCard = ({
const theme = useTheme();
return (
<Box minW={minW} bg={'white'} border={theme.borders.md} borderRadius={'md'} boxShadow={'sm'}>
<Box
minW={minW}
maxW={'430px'}
bg={'white'}
border={theme.borders.md}
borderRadius={'md'}
boxShadow={'sm'}
>
<Flex className="custom-drag-handle" px={4} py={3} alignItems={'center'}>
<Avatar src={logo} borderRadius={'md'} objectFit={'contain'} w={'30px'} h={'30px'} />
<Box ml={3} fontSize={'lg'} color={'myGray.600'}>
@@ -39,7 +46,7 @@ const NodeCard = ({
<MyTooltip label={description} forceShow>
<QuestionOutlineIcon
display={['none', 'inline']}
transform={'translateY(-1px)'}
transform={'translateY(1px)'}
ml={1}
/>
</MyTooltip>

View File

@@ -68,6 +68,9 @@ const NodeVariable = dynamic(() => import('./components/Nodes/NodeVariable'), {
const NodeUserGuide = dynamic(() => import('./components/Nodes/NodeUserGuide'), {
ssr: false
});
const NodeExtract = dynamic(() => import('./components/Nodes/NodeExtract'), {
ssr: false
});
import 'reactflow/dist/style.css';
import styles from './index.module.scss';
@@ -83,7 +86,8 @@ const nodeTypes = {
[FlowModuleTypeEnum.kbSearchNode]: NodeKbSearch,
[FlowModuleTypeEnum.tfSwitchNode]: NodeTFSwitch,
[FlowModuleTypeEnum.answerNode]: NodeAnswer,
[FlowModuleTypeEnum.classifyQuestion]: NodeCQNode
[FlowModuleTypeEnum.classifyQuestion]: NodeCQNode,
[FlowModuleTypeEnum.contentExtract]: NodeExtract
// [FlowModuleTypeEnum.empty]: EmptyModule
};
const edgeTypes = {

View File

@@ -8,13 +8,14 @@ import { countModelPrice } from '@/service/events/pushBill';
import { UserModelSchema } from '@/types/mongoSchema';
import { getModel } from '@/service/utils/data';
import { SystemInputEnum } from '@/constants/app';
import { SpecialInputKeyEnum } from '@/constants/flow';
export type CQProps = {
systemPrompt?: string;
history?: ChatItemType[];
[SystemInputEnum.userChatInput]: string;
userOpenaiAccount: UserModelSchema['openaiAccount'];
agents: ClassifyQuestionAgentItemType[];
[SpecialInputKeyEnum.agents]: ClassifyQuestionAgentItemType[];
};
export type CQResponse = {
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;

View File

@@ -1,50 +1,44 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
import { ChatContextFilter } from '@/service/utils/chat/index';
import type { ChatItemType } from '@/types/chat';
import { ChatRoleEnum } from '@/constants/chat';
import type { ChatHistoryItemResType, ChatItemType } from '@/types/chat';
import { ChatModuleEnum, ChatRoleEnum, TaskResponseKeyEnum } from '@/constants/chat';
import { getAIChatApi, axiosConfig } from '@/service/ai/openai';
import type { ClassifyQuestionAgentItemType } from '@/types/app';
import { SystemInputEnum } from '@/constants/app';
import type { ContextExtractAgentItemType } from '@/types/app';
import { ContextExtractEnum } from '@/constants/flow/flowField';
import { countModelPrice } from '@/service/events/pushBill';
import { UserModelSchema } from '@/types/mongoSchema';
import { getModel } from '@/service/utils/data';
export type Props = {
systemPrompt?: string;
userOpenaiAccount: UserModelSchema['openaiAccount'];
history?: ChatItemType[];
[SystemInputEnum.userChatInput]: string;
description: string;
agents: ClassifyQuestionAgentItemType[];
[ContextExtractEnum.content]: string;
[ContextExtractEnum.extractKeys]: ContextExtractAgentItemType[];
[ContextExtractEnum.description]: string;
};
export type Response = {
arguments: Record<string, any>;
deficiency: boolean;
[ContextExtractEnum.success]?: boolean;
[ContextExtractEnum.failed]?: boolean;
[ContextExtractEnum.fields]: Record<string, any>;
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
};
const agentModel = 'gpt-3.5-turbo';
const agentFunName = 'agent_extract_data';
const maxTokens = 3000;
export async function extract({
systemPrompt,
agents,
export async function dispatchContentExtract({
userOpenaiAccount,
content,
extractKeys,
history = [],
userChatInput,
description
}: Props): Promise<Response> {
const messages: ChatItemType[] = [
...(systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: systemPrompt
}
]
: []),
...history,
{
obj: ChatRoleEnum.Human,
value: userChatInput
value: content
}
];
const filterMessages = ChatContextFilter({
@@ -62,25 +56,25 @@ export async function extract({
description: string;
}
> = {};
agents.forEach((item) => {
extractKeys.forEach((item) => {
properties[item.key] = {
type: 'string',
description: item.value
description: item.desc
};
});
// function body
const agentFunction = {
name: agentFunName,
description,
description: `${description}\n如果内容不存在返回空字符串。当前时间是2023/7/31 18:00`,
parameters: {
type: 'object',
properties,
required: agents.map((item) => item.key)
required: extractKeys.map((item) => item.key)
}
};
const chatAPI = getAIChatApi();
const chatAPI = getAIChatApi(userOpenaiAccount);
const response = await chatAPI.createChatCompletion(
{
@@ -91,21 +85,44 @@ export async function extract({
functions: [agentFunction]
},
{
...axiosConfig()
...axiosConfig(userOpenaiAccount)
}
);
const arg = JSON.parse(response.data.choices?.[0]?.message?.function_call?.arguments || '{}');
let deficiency = false;
for (const key in arg) {
if (arg[key] === '') {
deficiency = true;
break;
const arg: Record<string, any> = (() => {
try {
return JSON.parse(response.data.choices?.[0]?.message?.function_call?.arguments || '{}');
} catch (error) {
return {};
}
})();
console.log(adaptMessages, arg);
// auth fields
let success = !extractKeys.find((item) => !arg[item.key]);
// auth empty value
if (success) {
for (const key in arg) {
if (arg[key] === '') {
success = false;
break;
}
}
}
const tokens = response.data.usage?.total_tokens || 0;
return {
arguments: arg,
deficiency
[ContextExtractEnum.success]: success ? true : undefined,
[ContextExtractEnum.failed]: success ? undefined : true,
[ContextExtractEnum.fields]: arg,
[TaskResponseKeyEnum.responseData]: {
moduleName: ChatModuleEnum.Extract,
price: userOpenaiAccount?.key ? 0 : countModelPrice({ model: agentModel, tokens }),
model: getModel(agentModel)?.name || agentModel,
tokens,
extractDescription: description,
extractResult: arg
}
};
}

View File

@@ -8,6 +8,7 @@ import {
import type { FlowInputItemType, FlowOutputItemType, FlowOutputTargetItemType } from './flow';
import type { AppSchema, kbSchema } from './mongoSchema';
import { ChatModelType } from '@/constants/model';
import { FlowValueTypeEnum } from '@/constants/flow';
export type AppListItemType = {
_id: string;
@@ -45,6 +46,11 @@ export type ClassifyQuestionAgentItemType = {
value: string;
key: string;
};
export type ContextExtractAgentItemType = {
desc: string;
key: string;
required: boolean;
};
export type VariableItemType = {
id: string;

View File

@@ -66,4 +66,8 @@ export type ChatHistoryItemResType = {
// cq
cqList?: ClassifyQuestionAgentItemType[];
cqResult?: string;
// content extract
extractDescription?: string;
extractResult?: Record<string, any>;
};