diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 3c27c637f..e296efe6f 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -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", diff --git a/client/public/locales/zh/common.json b/client/public/locales/zh/common.json index f3359312c..d776bc4db 100644 --- a/client/public/locales/zh/common.json +++ b/client/public/locales/zh/common.json @@ -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": "立即开始", diff --git a/client/src/constants/chat.ts b/client/src/constants/chat.ts index 819bbac0f..9a64defee 100644 --- a/client/src/constants/chat.ts +++ b/client/src/constants/chat.ts @@ -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 { diff --git a/client/src/constants/flow/ModuleTemplate.ts b/client/src/constants/flow/ModuleTemplate.ts index b33256b74..7b993f05d 100644 --- a/client/src/constants/flow/ModuleTemplate.ts +++ b/client/src/constants/flow/ModuleTemplate.ts @@ -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: '打招呼、问候等问题', diff --git a/client/src/constants/flow/flowField.ts b/client/src/constants/flow/flowField.ts new file mode 100644 index 000000000..0f7be6037 --- /dev/null +++ b/client/src/constants/flow/flowField.ts @@ -0,0 +1,8 @@ +export enum ContextExtractEnum { + extractKeys = 'extractKeys', + content = 'content', + description = 'description', + success = 'success', + failed = 'failed', + fields = 'fields' +} diff --git a/client/src/constants/flow/index.ts b/client/src/constants/flow/index.ts index 1fb92be77..2e34718fd 100644 --- a/client/src/constants/flow/index.ts +++ b/client/src/constants/flow/index.ts @@ -35,7 +35,8 @@ export enum FlowModuleTypeEnum { } export enum SpecialInputKeyEnum { - 'answerText' = 'text' + 'answerText' = 'text', + 'agents' = 'agents' // cq agent key } export enum FlowValueTypeEnum { diff --git a/client/src/pages/api/openapi/v1/chat/completions.ts b/client/src/pages/api/openapi/v1/chat/completions.ts index 0d307b86e..dbf8cbf4e 100644 --- a/client/src/pages/api/openapi/v1/chat/completions.ts +++ b/client/src/pages/api/openapi/v1/chat/completions.ts @@ -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); diff --git a/client/src/pages/app/detail/components/AdEdit/components/Nodes/NodeCQNode.tsx b/client/src/pages/app/detail/components/AdEdit/components/Nodes/NodeCQNode.tsx index 3bd77dd9b..438a841b0 100644 --- a/client/src/pages/app/detail/components/AdEdit/components/Nodes/NodeCQNode.tsx +++ b/client/src/pages/app/detail/components/AdEdit/components/Nodes/NodeCQNode.tsx @@ -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 = [] }: { diff --git a/client/src/pages/app/detail/components/AdEdit/components/Nodes/NodeExtract.tsx b/client/src/pages/app/detail/components/AdEdit/components/Nodes/NodeExtract.tsx new file mode 100644 index 000000000..1549e5bbb --- /dev/null +++ b/client/src/pages/app/detail/components/AdEdit/components/Nodes/NodeExtract.tsx @@ -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) => { + const { t } = useTranslation(); + const [editExtractFiled, setEditExtractField] = useState(); + + return ( + + + + ( + + + + + + + + + + + + + {extractKeys.map((item, index) => ( + + + + + + + ))} + +
字段 key字段描述必填
{item.key} + {item.desc}{' '} + {item.required ? '✔' : ''} + { + setEditExtractField(item); + }} + /> + { + onChangeNode({ + moduleId, + type: 'inputs', + key: ContextExtractEnum.extractKeys, + value: extractKeys.filter((extract) => item.key !== extract.key) + }); + }} + /> +
+
+ + + +
+ ) + }} + /> +
+ + + + + + {!!editExtractFiled && ( + 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); + }} + /> + )} +
+ ); +}; + +export default NodeExtract; diff --git a/client/src/pages/app/detail/components/AdEdit/components/modules/Divider.tsx b/client/src/pages/app/detail/components/AdEdit/components/modules/Divider.tsx index a74ee3ab5..b4f7db0b7 100644 --- a/client/src/pages/app/detail/components/AdEdit/components/modules/Divider.tsx +++ b/client/src/pages/app/detail/components/AdEdit/components/modules/Divider.tsx @@ -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 ( { borderBottom={theme.borders.base} fontSize={'lg'} > - {text} + {t(`common.${text}`)} ); }; diff --git a/client/src/pages/app/detail/components/AdEdit/components/modules/ExtractFieldModal.tsx b/client/src/pages/app/detail/components/AdEdit/components/modules/ExtractFieldModal.tsx new file mode 100644 index 000000000..62a32bde7 --- /dev/null +++ b/client/src/pages/app/detail/components/AdEdit/components/modules/ExtractFieldModal.tsx @@ -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({ + defaultValues: defaultField + }); + + return ( + + + + 提取字段配置 + + + + 必填 + + + + 字段描述 + + + + 字段 key + + + + + + + + + + ); +}; + +export default React.memo(ExtractFieldModal); diff --git a/client/src/pages/app/detail/components/AdEdit/components/modules/NodeCard.tsx b/client/src/pages/app/detail/components/AdEdit/components/modules/NodeCard.tsx index bd24bd52b..89fc3e099 100644 --- a/client/src/pages/app/detail/components/AdEdit/components/modules/NodeCard.tsx +++ b/client/src/pages/app/detail/components/AdEdit/components/modules/NodeCard.tsx @@ -29,7 +29,14 @@ const NodeCard = ({ const theme = useTheme(); return ( - + @@ -39,7 +46,7 @@ const NodeCard = ({ diff --git a/client/src/pages/app/detail/components/AdEdit/index.tsx b/client/src/pages/app/detail/components/AdEdit/index.tsx index 14a00940c..d7e04a18c 100644 --- a/client/src/pages/app/detail/components/AdEdit/index.tsx +++ b/client/src/pages/app/detail/components/AdEdit/index.tsx @@ -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 = { diff --git a/client/src/service/moduleDispatch/agent/classifyQuestion.ts b/client/src/service/moduleDispatch/agent/classifyQuestion.ts index fb2cefb78..350c4ee36 100644 --- a/client/src/service/moduleDispatch/agent/classifyQuestion.ts +++ b/client/src/service/moduleDispatch/agent/classifyQuestion.ts @@ -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; diff --git a/client/src/service/moduleDispatch/agent/extract.ts b/client/src/service/moduleDispatch/agent/extract.ts index 3fa3c4c20..e096b3bed 100644 --- a/client/src/service/moduleDispatch/agent/extract.ts +++ b/client/src/service/moduleDispatch/agent/extract.ts @@ -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; - deficiency: boolean; + [ContextExtractEnum.success]?: boolean; + [ContextExtractEnum.failed]?: boolean; + [ContextExtractEnum.fields]: Record; + [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 { 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 = (() => { + 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 + } }; } diff --git a/client/src/types/app.d.ts b/client/src/types/app.d.ts index 24e2c70eb..b22e8ddaf 100644 --- a/client/src/types/app.d.ts +++ b/client/src/types/app.d.ts @@ -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; diff --git a/client/src/types/chat.d.ts b/client/src/types/chat.d.ts index 4204d128a..af4d7e878 100644 --- a/client/src/types/chat.d.ts +++ b/client/src/types/chat.d.ts @@ -66,4 +66,8 @@ export type ChatHistoryItemResType = { // cq cqList?: ClassifyQuestionAgentItemType[]; cqResult?: string; + + // content extract + extractDescription?: string; + extractResult?: Record; };