diff --git a/client/src/api/fetch.ts b/client/src/api/fetch.ts index bb5938070..1fdecea8a 100644 --- a/client/src/api/fetch.ts +++ b/client/src/api/fetch.ts @@ -12,7 +12,7 @@ export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) new Promise( async (resolve, reject) => { try { - const response = await window.fetch('/api/openapi/v1/chat/completions', { + const response = await window.fetch('/api/openapi/v1/chat/test', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -74,8 +74,9 @@ export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) responseText += answer; } else if (item.event === sseResponseEventEnum.chatResponse) { const chatResponse = data as ChatResponseType; - newChatId = chatResponse.newChatId; - quoteLen = chatResponse.quoteLen || 0; + newChatId = + chatResponse.newChatId !== undefined ? chatResponse.newChatId : newChatId; + quoteLen = chatResponse.quoteLen !== undefined ? chatResponse.quoteLen : quoteLen; } else if (item.event === sseResponseEventEnum.error) { errMsg = getErrText(data, '流响应错误'); } diff --git a/client/src/constants/app.ts b/client/src/constants/app.ts new file mode 100644 index 000000000..3769a274b --- /dev/null +++ b/client/src/constants/app.ts @@ -0,0 +1,886 @@ +import type { ModuleItemCommonType, ModuleItemType, AppItemType } from '@/types/app'; + +/* flow module */ +export enum ModuleInputItemTypeEnum { + system = 'system', + numberInput = 'numberInput', + select = 'select', + slider = 'slider' +} +export enum ModulesInputItemTypeEnum { + system = 'system' +} + +export const HistoryInputModule: ModuleItemCommonType = { + key: 'history', + label: '聊天记录', + description: '', + formType: ModuleInputItemTypeEnum.system +}; +export const UserInputModule: ModuleItemCommonType = { + key: 'userChatInput', + label: '用户输入', + description: '', + formType: ModuleInputItemTypeEnum.system +}; + +export const UserChatInputModule: ModuleItemType = { + moduleId: '', + avatar: '/imgs/logo.png', + name: '用户问题输入', + description: '', + url: '', + body: [], + inputs: [UserInputModule], + outputs: [ + { + key: 'chatInput', + targets: [] + } + ] +}; + +export const HistoryModule: ModuleItemType = { + moduleId: '', + avatar: '/imgs/logo.png', + name: '聊天记录', + description: '', + url: '/openapi/chat/getHistory', + body: [ + { + key: 'historyLen', + label: '最大记录数', + formType: ModuleInputItemTypeEnum.numberInput, + placeholder: '', + max: 30, + min: 0, + default: 10 + } + ], + inputs: [ + { + key: 'chatId', + label: '聊天框ID', + formType: ModuleInputItemTypeEnum.system + } + ], + outputs: [ + { + key: 'history', + targets: [] + } + ] +}; + +export const OpenAIChatModule: ModuleItemType = { + moduleId: '', + avatar: '/imgs/logo.png', + name: 'GPT 对话', + description: '', + url: '/openapi/chat/completion', + body: [ + { + key: 'model', + label: '模型', + formType: ModuleInputItemTypeEnum.select, + placeholder: '', + enum: [ + { label: 'Gpt35-4k', value: 'gpt-3.5-turbo' }, + { label: 'Gpt35-16k', value: 'gpt-3.5-turbo-16k' }, + { label: 'Gpt4', value: 'gpt-4' } + ] + }, + { + key: 'temperature', + label: '温度', + formType: ModuleInputItemTypeEnum.slider, + enum: [ + { label: '严谨', value: 0 }, + { label: '发散', value: 10 } + ], + max: 10, + min: 0 + }, + { + key: 'maxToken', + label: '回复上限', + formType: ModuleInputItemTypeEnum.slider, + enum: [ + { label: '严谨', value: 0 }, + { label: '发散', value: 10 } + ], + max: 10, + min: 0 + } + ], + inputs: [HistoryInputModule, UserInputModule], + outputs: [ + { + key: 'history', + targets: [] + }, + { + key: 'jsonRes', + targets: [] + } + ] +}; + +/* app */ +export enum AppModuleItemTypeEnum { + 'http' = 'http', // send a http request + 'switch' = 'switch', // one input and two outputs + 'answer' = 'answer' // redirect response +} +export enum SystemInputEnum { + 'start' = 'start', // a trigger switch + 'history' = 'history', + 'userChatInput' = 'userChatInput' +} +export enum SpecificInputEnum { + 'answerText' = 'answerText' // answer module text key +} + +export const answerModule = ({ id, defaultText }: { id: string; defaultText?: string }) => ({ + moduleId: id, + type: AppModuleItemTypeEnum.answer, + body: {}, + inputs: [ + { + key: SpecificInputEnum.answerText, + value: defaultText + }, + ...(defaultText !== undefined + ? [ + { + key: SystemInputEnum.start, + value: undefined + } + ] + : []) + ], + outputs: [] +}); + +export const chatAppDemo: AppItemType = { + id: 'chat', + // 标记字段 + modules: [ + { + moduleId: '1', + type: AppModuleItemTypeEnum.http, + url: '/openapi/modules/chat/gpt', + body: { + model: 'gpt-3.5-turbo-16k', + temperature: 5, + maxToken: 4000 + }, + inputs: [ + { + key: SystemInputEnum.history, + value: undefined + }, + { + key: SystemInputEnum.userChatInput, + value: undefined + } + ], + outputs: [ + { + key: 'answer', + answer: true, + value: undefined, + targets: [] + } + ] + } + ] +}; + +export const kbChatAppDemo: AppItemType = { + id: 'kbchat', + // 标记字段 + modules: [ + { + moduleId: '1', + type: 'http', + url: '/openapi/modules/kb/search', + body: { + kb_ids: ['646627f4f7b896cfd8910e38'], + similarity: 0.82, + limit: 2, + maxToken: 2500 + }, + inputs: [ + { + key: SystemInputEnum.history, + value: undefined + }, + { + key: SystemInputEnum.userChatInput, + value: undefined + } + ], + outputs: [ + { + key: 'rawSearch', + value: undefined, + targets: [] + }, + { + key: 'isEmpty', + value: undefined, + targets: [ + { + moduleId: '4', + key: 'switch' + } + ] + }, + { + key: 'quotePrompt', + response: true, + value: undefined, + targets: [ + { + moduleId: '2', + key: 'quotePrompt' + } + ] + } + ] + }, + { + moduleId: '4', + type: 'switch', + body: {}, + inputs: [ + { + key: 'switch', + value: undefined + } + ], + outputs: [ + { + key: 'true', + value: undefined, + targets: [ + { + moduleId: '3', + key: SystemInputEnum.start + } + ] + }, + { + key: 'false', + value: undefined, + targets: [ + { + moduleId: '2', + key: SystemInputEnum.start + } + ] + } + ] + }, + { + moduleId: '2', + type: 'http', + url: '/openapi/modules/chat/gpt', + body: { + model: 'gpt-3.5-turbo-16k', + temperature: 5, + maxToken: 4000, + systemPrompt: '知识库是关于电影玲芽之旅的介绍。', + limitPrompt: '你仅回答关于电影《玲芽之旅的问题》' + }, + inputs: [ + { + key: SystemInputEnum.start, + value: undefined + }, + { + key: 'quotePrompt', + value: undefined + }, + { + key: SystemInputEnum.history, + value: undefined + }, + { + key: SystemInputEnum.userChatInput, + value: undefined + } + ], + outputs: [ + { + key: 'answer', + value: undefined, + answer: true, + targets: [] + } + ] + }, + answerModule({ id: '3', defaultText: '你好,我可以回答你关于电影《玲芽之旅》的问题。' }) + ] +}; + +export const classifyQuestionDemo: AppItemType = { + id: 'classifyQuestionDemo', + // 标记字段 + modules: [ + { + moduleId: '1', + type: AppModuleItemTypeEnum.http, + url: '/openapi/modules/agent/classifyQuestion', + body: { + systemPrompt: + 'laf 一个云函数开发平台,提供了基于 Node 的 serveless 的快速开发和部署。是一个集「函数计算」、「数据库」、「对象存储」等于一身的一站式开发平台。支持云函数、云数据库、在线编程 IDE、触发器、云存储和静态网站托管等功能。', + agents: [ + { + desc: '打招呼、问候、身份询问等问题', + key: 'a' + }, + { + desc: "询问 'laf 使用和介绍的问题'", + key: 'b' + }, + { + desc: "询问 'laf 代码问题'", + key: 'c' + }, + { + desc: '其他问题', + key: 'd' + } + ] + }, + inputs: [ + { + key: SystemInputEnum.history, + value: undefined + }, + { + key: SystemInputEnum.userChatInput, + value: undefined + } + ], + outputs: [ + { + key: 'a', + value: undefined, + targets: [ + { + moduleId: 'a', + key: SystemInputEnum.start + } + ] + }, + { + key: 'b', + value: undefined, + targets: [ + { + moduleId: 'b', + key: SystemInputEnum.start + } + ] + }, + { + key: 'c', + value: undefined, + targets: [ + { + moduleId: 'c', + key: SystemInputEnum.start + } + ] + }, + { + key: 'd', + value: undefined, + targets: [ + { + moduleId: 'd', + key: SystemInputEnum.start + } + ] + } + ] + }, + { + moduleId: 'a', + type: 'answer', + body: {}, + inputs: [ + { + key: SpecificInputEnum.answerText, + value: '你好,我是 Laf 助手,有什么可以帮助你的?' + }, + { + key: SystemInputEnum.start, + value: undefined + } + ], + outputs: [] + }, + // laf 知识库 + { + moduleId: 'b', + type: 'http', + url: '/openapi/modules/kb/search', + body: { + kb_ids: ['646627f4f7b896cfd8910e24'], + similarity: 0.82, + limit: 4, + maxToken: 2500 + }, + inputs: [ + { + key: SystemInputEnum.start, + value: undefined + }, + { + key: SystemInputEnum.history, + value: undefined + }, + { + key: SystemInputEnum.userChatInput, + value: undefined + } + ], + outputs: [ + { + key: 'rawSearch', + value: undefined, + response: true, + targets: [] + }, + { + key: 'quotePrompt', + value: undefined, + targets: [ + { + moduleId: 'lafchat', + key: 'quotePrompt' + } + ] + } + ] + }, + // laf 对话 + { + moduleId: 'lafchat', + type: 'http', + url: '/openapi/modules/chat/gpt', + body: { + model: 'gpt-3.5-turbo-16k', + temperature: 5, + maxToken: 4000, + systemPrompt: '知识库是关于 Laf 的内容。', + limitPrompt: '你仅能参考知识库的内容回答问题,不能超出知识库范围。' + }, + inputs: [ + { + key: 'quotePrompt', + value: undefined + }, + { + key: SystemInputEnum.history, + value: undefined + }, + { + key: SystemInputEnum.userChatInput, + value: undefined + } + ], + outputs: [ + { + key: 'answer', + answer: true, + value: undefined, + targets: [] + } + ] + }, + // laf 代码知识库 + { + moduleId: 'c', + type: 'http', + url: '/openapi/modules/kb/search', + body: { + kb_ids: ['646627f4f7b896cfd8910e26'], + similarity: 0.8, + limit: 4, + maxToken: 2500 + }, + inputs: [ + { + key: SystemInputEnum.start, + value: undefined + }, + { + key: SystemInputEnum.history, + value: undefined + }, + { + key: SystemInputEnum.userChatInput, + value: undefined + } + ], + outputs: [ + { + key: 'rawSearch', + value: undefined, + response: true, + targets: [] + }, + { + key: 'quotePrompt', + value: undefined, + targets: [ + { + moduleId: 'lafcodechat', + key: 'quotePrompt' + } + ] + } + ] + }, + // laf代码对话 + { + moduleId: 'lafcodechat', + type: 'http', + url: '/openapi/modules/chat/gpt', + body: { + model: 'gpt-3.5-turbo-16k', + temperature: 5, + maxToken: 4000, + systemPrompt: `下例是laf结构\n~~~ts\nimport cloud from '@lafjs/cloud'\nexport default async function(ctx: FunctionContext){\nreturn \"success\"\n};\n~~~\n下例是@lafjs/cloud的api\n~~~\ncloud.fetch//完全等同axios\ncloud.database()// 获取操作数据库实例,和mongo语法相似.\ncloud.getToken(payload)//获取token\ncloud.parseToken(token)//解析token\n// 下面是持久化缓存Api\ncloud.shared.set(key,val); //设置缓存,仅能设置值,无法设置过期时间\ncloud.shared.get(key);\ncloud.shared.has(key); \ncloud.shared.delete(key); \ncloud.shared.clear(); \n~~~\n下例是ctx对象\n~~~\nctx.requestId\nctx.method\nctx.headers//请求的 headers, ctx.headers.get('Content-Type')获取Content-Type的值\nctx.user//Http Bearer Token 认证时,获取token值\nctx.query\nctx.body\nctx.request//同express的Request\nctx.response//同express的Response\nctx.socket/WebSocket 实例\nctx.files//上传的文件 (File对象数组)\nctx.env//自定义的环境变量\n~~~\n下例是数据库获取数据\n~~~ts\nconst db = cloud.database();\nexport default async function(ctx: FunctionContext){\nconst {minMemory} = ctx.query\nconst _ = db.command;\nconst {data: users,total} = collection(\"users\")\n .where({//条件查询\n category: \"computer\",\n type: {\n memory: _gt(minMemory), \n }\n }) \n .skip(10)//跳过10条-分页时使用\n .limit(10)//仅返回10条\n .orderBy(\"name\", \"asc\") \n .orderBy(\"age\", \"desc\")\n .field({age:true,name: false})//返回age不返回name\n}\nconst {data:user} = db.where({phone:req.body.phone}).getOne()//获取一个满足条件的用户\nreturn {users,total}\n~~~\n下例是数据库添加数据\n~~~ts\nconst db = cloud.database();\nexport default async function(ctx: FunctionContext) {\n const {username} = ctx.body\n const {id:userId, ok} = await collection(\"users\")\n .add({\n username, \n })\n if(ok) return {userId}\n return {code:500,message:\"失败\"}\n}\n~~~\n下例是数据库更新数据\n~~~ts\nconst db = cloud.database();\nexport default async function(ctx: FunctionContext){\nconst {id} = req.query\n//id直接修改\nawait collection(\"user\").doc(\"id\").update({\n name: \"Hey\",\n});\n//批量更新\nawait collection\n .where({name:\"1234\"})\n .update({\n age:18\n })\nconst _ = db.command;\nawait collection(\"user\")\n .doc(id)\n .set({\n count: _.inc(1)\n count: _.mul(2)\n count: _.remove()\n users: _.push([\"aaa\", \"bbb\"])\n users: _.push(\"aaa\")\n users: _.pop()\n users: _.unshift()\n users: _.shift()\n })\n}\n~~~\n下例是删除数据库记录\n~~~ts\nconst db = cloud.database();\nexport default async function(ctx: FunctionContext){\nconst {id} = req.query\ncollection(\"user\").doc(id).remove();\n//批量删除\ncollection\n .where({age:18}) \n .remove({multi: true})\nreturn \"success\"\n}\n~~~\n你只需返回 ts 代码块!不需要说明.\n用户的问题与 Laf 代码无关时,你直接回答: \"我不确定,我只会写 Laf 代码。\"`, + limitPrompt: + '你是由 Laf 团队开发的代码助手,把我的需求用 Laf 代码实现.参考知识库中 Laf 的例子.' + }, + inputs: [ + { + key: 'quotePrompt', + value: undefined + }, + { + key: SystemInputEnum.history, + value: undefined + }, + { + key: SystemInputEnum.userChatInput, + value: undefined + } + ], + outputs: [ + { + key: 'answer', + answer: true, + value: undefined, + targets: [] + } + ] + }, + { + moduleId: 'd', + type: 'answer', + body: {}, + inputs: [ + { + key: SpecificInputEnum.answerText, + value: '你好,我没有理解你的意思,请问你有什么 Laf 相关的问题么?' + }, + { + key: SystemInputEnum.start, + value: undefined + } + ], + outputs: [] + } + ] +}; + +export const lafClassifyQuestionDemo: AppItemType = { + id: 'test', + // 标记字段 + modules: [ + { + moduleId: '1', + type: AppModuleItemTypeEnum.http, + url: '/openapi/modules/agent/classifyQuestion', + body: { + systemPrompt: + 'laf 一个云函数开发平台,提供了基于 Node 的 serveless 的快速开发和部署。是一个集「函数计算」、「数据库」、「对象存储」等于一身的一站式开发平台。支持云函数、云数据库、在线编程 IDE、触发器、云存储和静态网站托管等功能。\nsealos是一个 k8s 云平台,可以让用户快速部署云服务。', + agents: [ + { + desc: '打招呼、问候、身份询问等问题', + key: 'a' + }, + { + desc: "询问 'laf 的使用和介绍'", + key: 'b' + }, + { + desc: "询问 'laf 代码相关问题'", + key: 'c' + }, + { + desc: "用户希望运行或知道 'laf 代码' 运行结果", + key: 'g' + }, + { + desc: "询问 'sealos 相关问题'", + key: 'd' + }, + { + desc: '其他问题', + key: 'e' + }, + { + desc: '商务类问题', + key: 'f' + } + ] + }, + inputs: [ + { + key: SystemInputEnum.history, + value: undefined + }, + { + key: SystemInputEnum.userChatInput, + value: undefined + } + ], + outputs: [ + { + key: 'a', + value: undefined, + targets: [ + { + moduleId: 'a', + key: SystemInputEnum.start + } + ] + }, + { + key: 'b', + value: undefined, + targets: [ + { + moduleId: 'b', + key: SystemInputEnum.start + } + ] + }, + { + key: 'c', + value: undefined, + targets: [ + { + moduleId: 'c', + key: SystemInputEnum.start + } + ] + }, + { + key: 'd', + value: undefined, + targets: [ + { + moduleId: 'd', + key: SystemInputEnum.start + } + ] + }, + { + key: 'e', + value: undefined, + targets: [ + { + moduleId: 'e', + key: SystemInputEnum.start + } + ] + }, + { + key: 'f', + value: undefined, + targets: [ + { + moduleId: 'f', + key: SystemInputEnum.start + } + ] + }, + { + key: 'g', + value: undefined, + targets: [ + { + moduleId: 'g', + key: SystemInputEnum.start + } + ] + } + ] + }, + { + moduleId: 'a', + type: 'answer', + body: {}, + inputs: [ + { + key: SpecificInputEnum.answerText, + value: '你好,我是 环界云 助手,你有什么 Laf 或者 sealos 的 问题么?' + }, + { + key: SystemInputEnum.start, + value: undefined + } + ], + outputs: [] + }, + { + moduleId: 'b', + type: 'answer', + body: {}, + inputs: [ + { + key: SpecificInputEnum.answerText, + value: '查询 Laf 通用知识库:xxxxx' + }, + { + key: SystemInputEnum.start, + value: undefined + } + ], + outputs: [] + }, + { + moduleId: 'c', + type: 'answer', + body: {}, + inputs: [ + { + key: SpecificInputEnum.answerText, + value: '查询 Laf 代码知识库:xxxxx' + }, + { + key: SystemInputEnum.start, + value: undefined + } + ], + outputs: [] + }, + { + moduleId: 'd', + type: 'answer', + body: {}, + inputs: [ + { + key: SpecificInputEnum.answerText, + value: '查询 sealos 通用知识库: xxxx' + }, + { + key: SystemInputEnum.start, + value: undefined + } + ], + outputs: [] + }, + { + moduleId: 'e', + type: 'answer', + body: {}, + inputs: [ + { + key: SpecificInputEnum.answerText, + value: '其他问题。回复引导语:xxxx' + }, + { + key: SystemInputEnum.start, + value: undefined + } + ], + outputs: [] + }, + { + moduleId: 'f', + type: 'answer', + body: {}, + inputs: [ + { + key: SpecificInputEnum.answerText, + value: '商务类问题,联系方式:xxxxx' + }, + { + key: SystemInputEnum.start, + value: undefined + } + ], + outputs: [] + }, + { + moduleId: 'g', + type: 'http', + url: '/openapi/modules/agent/extract', + body: { + description: '运行 laf 代码', + agents: [ + { + desc: '代码内容', + key: 'code' + } + ] + }, + inputs: [ + { + key: SystemInputEnum.start, + value: undefined + }, + { + key: SystemInputEnum.history, + value: undefined + }, + { + key: SystemInputEnum.userChatInput, + value: undefined + } + ], + outputs: [ + { + key: 'code', + value: undefined, + targets: [ + { + moduleId: 'code_run', + key: 'code' + } + ] + } + ] + }, + { + moduleId: 'code_run', + type: AppModuleItemTypeEnum.http, + url: 'https://v1cde7.laf.run/tess', + body: {}, + inputs: [ + { + key: 'code', + value: undefined + } + ], + outputs: [ + { + key: 'star', + value: undefined, + targets: [] + } + ] + } + ] +}; diff --git a/client/src/constants/chat.ts b/client/src/constants/chat.ts index 541429ce3..f51641db5 100644 --- a/client/src/constants/chat.ts +++ b/client/src/constants/chat.ts @@ -1,7 +1,9 @@ export enum sseResponseEventEnum { error = 'error', answer = 'answer', - chatResponse = 'chatResponse' + chatResponse = 'chatResponse', // + appStreamResponse = 'appStreamResponse', // sse response request + moduleFetchResponse = 'moduleFetchResponse' // http module sse response } export enum ChatRoleEnum { diff --git a/client/src/pages/api/chat/saveChat.ts b/client/src/pages/api/chat/saveChat.ts index 238befb9e..e196cc5b3 100644 --- a/client/src/pages/api/chat/saveChat.ts +++ b/client/src/pages/api/chat/saveChat.ts @@ -47,7 +47,7 @@ export async function saveChat({ modelId, prompts, userId -}: Props & { newChatId?: Types.ObjectId; userId: string }) { +}: Props & { newChatId?: Types.ObjectId; userId: string }): Promise<{ newChatId: string }> { await connectToDatabase(); const { model } = await authModel({ modelId, userId, authOwner: false }); @@ -104,6 +104,7 @@ export async function saveChat({ ]); return { - ...response + // @ts-ignore + newChatId: response?.newChatId || '' }; } diff --git a/client/src/pages/api/openapi/modules/agent/classifyQuestion.ts b/client/src/pages/api/openapi/modules/agent/classifyQuestion.ts new file mode 100644 index 000000000..c4274b5e4 --- /dev/null +++ b/client/src/pages/api/openapi/modules/agent/classifyQuestion.ts @@ -0,0 +1,114 @@ +// 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 { getOpenAIApi, axiosConfig } from '@/service/ai/openai'; +import type { ClassifyQuestionAgentItemType } from '@/types/app'; + +export type Props = { + systemPrompt?: string; + history?: ChatItemType[]; + userChatInput: string; + agents: ClassifyQuestionAgentItemType[]; +}; +export type Response = { history: ChatItemType[] }; + +const agentModel = 'gpt-3.5-turbo-16k'; +const agentFunName = 'agent_user_question'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + let { systemPrompt, agents, history = [], userChatInput } = req.body as Props; + + const response = await classifyQuestion({ + systemPrompt, + history, + userChatInput, + agents + }); + + jsonRes(res, { + data: response + }); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} + +/* request openai chat */ +export async function classifyQuestion({ + agents, + systemPrompt, + history = [], + userChatInput +}: Props) { + const messages: ChatItemType[] = [ + ...(systemPrompt + ? [ + { + obj: ChatRoleEnum.System, + value: systemPrompt + } + ] + : []), + { + obj: ChatRoleEnum.Human, + value: userChatInput + } + ]; + const filterMessages = ChatContextFilter({ + // @ts-ignore + model: agentModel, + prompts: messages, + maxTokens: 1500 + }); + const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false }); + + // function body + const agentFunction = { + name: agentFunName, + description: '严格判断用户问题的类型', + parameters: { + type: 'object', + properties: { + type: { + type: 'string', + description: agents.map((item) => `${item.desc},返回: '${item.key}'`).join('; '), + enum: agents.map((item) => item.key) + } + }, + required: ['type'] + } + }; + const chatAPI = getOpenAIApi(); + + const response = await chatAPI.createChatCompletion( + { + model: agentModel, + temperature: 0, + messages: [...adaptMessages], + function_call: { name: agentFunName }, + functions: [agentFunction] + }, + { + ...axiosConfig() + } + ); + + const arg = JSON.parse(response.data.choices?.[0]?.message?.function_call?.arguments || ''); + + if (!arg.type) { + throw new Error(''); + } + console.log(adaptMessages, arg.type); + + return { + [arg.type]: 1 + }; +} diff --git a/client/src/pages/api/openapi/modules/agent/extract.ts b/client/src/pages/api/openapi/modules/agent/extract.ts new file mode 100644 index 000000000..50270079e --- /dev/null +++ b/client/src/pages/api/openapi/modules/agent/extract.ts @@ -0,0 +1,97 @@ +// 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 { getOpenAIApi, axiosConfig } from '@/service/ai/openai'; +import type { ClassifyQuestionAgentItemType } from '@/types/app'; + +export type Props = { + history?: ChatItemType[]; + userChatInput: string; + agents: ClassifyQuestionAgentItemType[]; + description: string; +}; +export type Response = { history: ChatItemType[] }; + +const agentModel = 'gpt-3.5-turbo-16k'; +const agentFunName = 'agent_extract_data'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const response = await extract(req.body); + + jsonRes(res, { + data: response + }); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} + +/* request openai chat */ +export async function extract({ agents, history = [], userChatInput, description }: Props) { + const messages: ChatItemType[] = [ + ...history.slice(-4), + { + obj: ChatRoleEnum.Human, + value: userChatInput + } + ]; + const filterMessages = ChatContextFilter({ + // @ts-ignore + model: agentModel, + prompts: messages, + maxTokens: 3000 + }); + const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false }); + + const properties: Record< + string, + { + type: string; + description: string; + } + > = {}; + agents.forEach((item) => { + properties[item.key] = { + type: 'string', + description: item.desc + }; + }); + + // function body + const agentFunction = { + name: agentFunName, + description, + parameters: { + type: 'object', + properties, + required: agents.map((item) => item.key) + } + }; + + const chatAPI = getOpenAIApi(); + + const response = await chatAPI.createChatCompletion( + { + model: agentModel, + temperature: 0, + messages: [...adaptMessages], + function_call: { name: agentFunName }, + functions: [agentFunction] + }, + { + ...axiosConfig() + } + ); + + const arg = JSON.parse(response.data.choices?.[0]?.message?.function_call?.arguments || ''); + + return arg; +} diff --git a/client/src/pages/api/openapi/modules/chat/gpt.ts b/client/src/pages/api/openapi/modules/chat/gpt.ts new file mode 100644 index 000000000..9351785ef --- /dev/null +++ b/client/src/pages/api/openapi/modules/chat/gpt.ts @@ -0,0 +1,257 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { sseResponse } from '@/service/utils/tools'; +import { ChatModelMap, OpenAiChatEnum } from '@/constants/model'; +import { adaptChatItem_openAI } from '@/utils/plugin/openai'; +import { modelToolMap } from '@/utils/plugin'; +import { ChatCompletionType, ChatContextFilter } from '@/service/utils/chat/index'; +import type { ChatItemType } from '@/types/chat'; +import { getSystemOpenAiKey } from '@/service/utils/auth'; +import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat'; +import { parseStreamChunk, textAdaptGptResponse } from '@/utils/adapt'; +import { getOpenAIApi, axiosConfig } from '@/service/ai/openai'; + +export type Props = { + model: `${OpenAiChatEnum}`; + temperature?: number; + maxToken?: number; + history?: ChatItemType[]; + userChatInput: string; + stream?: boolean; + quotePrompt?: string; + systemPrompt?: string; + limitPrompt?: string; +}; +export type Response = { history: ChatItemType[] }; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + let { + model, + stream = false, + temperature = 0, + maxToken = 4000, + history = [], + quotePrompt, + userChatInput, + systemPrompt, + limitPrompt + } = req.body as Props; + + // temperature adapt + const modelConstantsData = ChatModelMap[model]; + // FastGpt temperature range: 1~10 + temperature = +(modelConstantsData.maxTemperature * (temperature / 10)).toFixed(2); + + const response = await chatCompletion({ + res, + model, + temperature, + maxToken, + stream, + history, + userChatInput, + systemPrompt, + limitPrompt, + quotePrompt + }); + + if (stream) { + sseResponse({ + res, + event: sseResponseEventEnum.moduleFetchResponse, + data: JSON.stringify(response) + }); + res.end(); + } else { + jsonRes(res, { + data: response + }); + } + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} + +/* request openai chat */ +export async function chatCompletion({ + res, + model = OpenAiChatEnum.GPT35, + temperature, + maxToken = 4000, + stream, + history = [], + quotePrompt, + userChatInput, + systemPrompt, + limitPrompt +}: Props & { res: NextApiResponse }) { + const messages: ChatItemType[] = [ + ...(quotePrompt + ? [ + { + obj: ChatRoleEnum.System, + value: quotePrompt + } + ] + : []), + ...(systemPrompt + ? [ + { + obj: ChatRoleEnum.System, + value: systemPrompt + } + ] + : []), + ...history, + ...(limitPrompt + ? [ + { + obj: ChatRoleEnum.Human, + value: limitPrompt + } + ] + : []), + { + obj: ChatRoleEnum.Human, + value: userChatInput + } + ]; + const modelTokenLimit = ChatModelMap[model]?.contextMaxToken || 4000; + + const filterMessages = ChatContextFilter({ + model, + prompts: messages, + maxTokens: Math.ceil(modelTokenLimit - 300) // filter token. not response maxToken + }); + + const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false }); + const chatAPI = getOpenAIApi(); + console.log(adaptMessages); + + /* count response max token */ + const promptsToken = modelToolMap[model].countTokens({ + messages: filterMessages + }); + maxToken = maxToken + promptsToken > modelTokenLimit ? modelTokenLimit - promptsToken : maxToken; + + const response = await chatAPI.createChatCompletion( + { + model, + temperature: Number(temperature || 0), + max_tokens: maxToken, + messages: adaptMessages, + frequency_penalty: 0.5, // 越大,重复内容越少 + presence_penalty: -0.5, // 越大,越容易出现新内容 + stream + }, + { + timeout: stream ? 60000 : 480000, + responseType: stream ? 'stream' : 'json', + ...axiosConfig() + } + ); + + const { answer, totalTokens } = await (async () => { + if (stream) { + // sse response + const { answer } = await streamResponse({ res, response }); + // count tokens + const finishMessages = filterMessages.concat({ + obj: ChatRoleEnum.AI, + value: answer + }); + + const totalTokens = modelToolMap[model].countTokens({ + messages: finishMessages + }); + + return { + answer, + totalTokens + }; + } else { + const answer = stream ? '' : response.data.choices?.[0].message?.content || ''; + const totalTokens = stream ? 0 : response.data.usage?.total_tokens || 0; + + return { + answer, + totalTokens + }; + } + })(); + + // count price + const unitPrice = ChatModelMap[model]?.price || 3; + return { + answer + }; +} + +async function streamResponse({ res, response }: { res: NextApiResponse; response: any }) { + let answer = ''; + let error: any = null; + + const clientRes = async (data: string) => { + const { content = '' } = (() => { + try { + const json = JSON.parse(data); + const content: string = json?.choices?.[0].delta.content || ''; + error = json.error; + answer += content; + return { content }; + } catch (error) { + return {}; + } + })(); + + if (res.closed || error) return; + + if (data === '[DONE]') { + sseResponse({ + res, + event: sseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text: null, + finish_reason: 'stop' + }) + }); + sseResponse({ + res, + event: sseResponseEventEnum.answer, + data: '[DONE]' + }); + } else { + sseResponse({ + res, + event: sseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text: content + }) + }); + } + }; + + try { + for await (const chunk of response.data as any) { + if (res.closed) break; + const parse = parseStreamChunk(chunk); + parse.forEach((item) => clientRes(item.data)); + } + } catch (error) { + console.log('pipe error', error); + } + + if (error) { + console.log(error); + return Promise.reject(error); + } + + return { + answer + }; +} diff --git a/client/src/pages/api/openapi/modules/kb/search.ts b/client/src/pages/api/openapi/modules/kb/search.ts new file mode 100644 index 000000000..ea6f1127a --- /dev/null +++ b/client/src/pages/api/openapi/modules/kb/search.ts @@ -0,0 +1,115 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { PgClient } from '@/service/pg'; +import { withNextCors } from '@/service/utils/tools'; +import type { ChatItemType } from '@/types/chat'; +import { ChatRoleEnum } from '@/constants/chat'; +import { openaiEmbedding_system } from '../../plugin/openaiEmbedding'; +import { modelToolMap } from '@/utils/plugin'; + +export type QuoteItemType = { + id: string; + q: string; + a: string; + source?: string; +}; +type Props = { + kb_ids: string[]; + history: ChatItemType[]; + similarity: number; + limit: number; + maxToken: number; + userChatInput: string; + stream?: boolean; +}; +type Response = { + rawSearch: QuoteItemType[]; + isEmpty?: boolean; + quotePrompt: string; +}; + +export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { + kb_ids = [], + history = [], + similarity, + limit, + maxToken, + userChatInput + } = req.body as Props; + + if (!similarity || !Array.isArray(kb_ids)) { + throw new Error('params is error'); + } + + const result = await appKbSearch({ + kb_ids, + history, + similarity, + limit, + maxToken, + userChatInput + }); + + jsonRes(res, { + data: result + }); + } catch (err) { + console.log(err); + jsonRes(res, { + code: 500, + error: err + }); + } +}); + +export async function appKbSearch({ + kb_ids = [], + history = [], + similarity = 0.8, + limit = 5, + maxToken = 2500, + userChatInput +}: Props): Promise { + // get vector + const promptVector = await openaiEmbedding_system({ + input: [userChatInput] + }); + + // search kb + const res: any = await PgClient.query( + `BEGIN; + SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10}; + select id,q,a,source from modelData where kb_id IN (${kb_ids + .map((item) => `'${item}'`) + .join(',')}) AND vector <#> '[${promptVector[0]}]' < -${similarity} order by vector <#> '[${ + promptVector[0] + }]' limit ${limit}; + COMMIT;` + ); + + const searchRes: QuoteItemType[] = res?.[2]?.rows || []; + + // filter part quote by maxToken + const sliceResult = modelToolMap['gpt-3.5-turbo'] + .tokenSlice({ + maxToken, + messages: searchRes.map((item, i) => ({ + obj: ChatRoleEnum.System, + value: `${i + 1}: [${item.q}\n${item.a}]` + })) + }) + .map((item) => item.value) + .join('\n') + .trim(); + + // slice filterSearch + const rawSearch = searchRes.slice(0, sliceResult.length); + + return { + isEmpty: rawSearch.length === 0, + rawSearch, + quotePrompt: sliceResult ? `知识库:\n${sliceResult}` : '' + }; +} diff --git a/client/src/pages/api/openapi/modules/tools/httpRequest.ts b/client/src/pages/api/openapi/modules/tools/httpRequest.ts new file mode 100644 index 000000000..7f11aeff5 --- /dev/null +++ b/client/src/pages/api/openapi/modules/tools/httpRequest.ts @@ -0,0 +1,4 @@ +export type Props = { + url: string; + body: Record; +}; diff --git a/client/src/pages/api/openapi/plugin/openaiEmbedding.ts b/client/src/pages/api/openapi/plugin/openaiEmbedding.ts index 1daa0a860..4a215d1ee 100644 --- a/client/src/pages/api/openapi/plugin/openaiEmbedding.ts +++ b/client/src/pages/api/openapi/plugin/openaiEmbedding.ts @@ -81,3 +81,35 @@ export async function openaiEmbedding({ return result.vectors; } + +export async function openaiEmbedding_system({ input }: Props) { + const apiKey = getSystemOpenAiKey(); + + // 获取 chatAPI + const chatAPI = getOpenAIApi(apiKey); + + // 把输入的内容转成向量 + const result = await chatAPI + .createEmbedding( + { + model: embeddingModel, + input + }, + { + timeout: 60000, + ...axiosConfig(apiKey) + } + ) + .then((res) => { + if (!res.data?.usage?.total_tokens) { + // @ts-ignore + return Promise.reject(res.data?.error?.message || 'Embedding Error'); + } + return { + tokenLen: res.data.usage.total_tokens || 0, + vectors: res.data.data.map((item) => item.embedding) + }; + }); + + return result.vectors; +} diff --git a/client/src/pages/api/openapi/v1/chat/test.ts b/client/src/pages/api/openapi/v1/chat/test.ts new file mode 100644 index 000000000..938efb5d5 --- /dev/null +++ b/client/src/pages/api/openapi/v1/chat/test.ts @@ -0,0 +1,338 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { connectToDatabase } from '@/service/mongo'; +import { authUser, authModel, getApiKey, authShareChat } from '@/service/utils/auth'; +import { sseErrRes, jsonRes } from '@/service/response'; +import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat'; +import { withNextCors } from '@/service/utils/tools'; +import type { CreateChatCompletionRequest } from 'openai'; +import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt'; +import { getChatHistory } from './getHistory'; +import { saveChat } from '@/pages/api/chat/saveChat'; +import { sseResponse } from '@/service/utils/tools'; +import { type ChatCompletionRequestMessage } from 'openai'; +import { + kbChatAppDemo, + chatAppDemo, + lafClassifyQuestionDemo, + classifyQuestionDemo, + SpecificInputEnum, + AppModuleItemTypeEnum +} from '@/constants/app'; +import { Types } from 'mongoose'; +import { moduleFetch } from '@/service/api/request'; +import { AppModuleItemType } from '@/types/app'; + +export type MessageItemType = ChatCompletionRequestMessage & { _id?: string }; +type FastGptWebChatProps = { + chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history + appId?: string; +}; +type FastGptShareChatProps = { + password?: string; + shareId?: string; +}; +export type Props = CreateChatCompletionRequest & + FastGptWebChatProps & + FastGptShareChatProps & { + messages: MessageItemType[]; + stream?: boolean; + }; +export type ChatResponseType = { + newChatId: string; + quoteLen?: number; +}; + +/* 发送提示词 */ +export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) { + res.on('close', () => { + res.end(); + }); + res.on('error', () => { + console.log('error: ', 'request error'); + res.end(); + }); + + let { chatId, appId, shareId, password = '', stream = false, messages = [] } = req.body as Props; + + try { + if (!messages) { + throw new Error('Prams Error'); + } + if (!Array.isArray(messages)) { + throw new Error('messages is not array'); + } + + await connectToDatabase(); + let startTime = Date.now(); + + /* user auth */ + const { + userId, + appId: authAppid, + authType + } = await (shareId + ? authShareChat({ + shareId, + password + }) + : authUser({ req })); + + appId = appId ? appId : authAppid; + if (!appId) { + throw new Error('appId is empty'); + } + + // get history + const { history } = await getChatHistory({ chatId, userId }); + const prompts = history.concat(gptMessage2ChatType(messages)); + if (prompts[prompts.length - 1].obj === 'AI') { + prompts.pop(); + } + // user question + const prompt = prompts.pop(); + + if (!prompt) { + throw new Error('Question is empty'); + } + + /* start process */ + const modules = JSON.parse(JSON.stringify(classifyQuestionDemo.modules)); + + const { responseData, answerText } = await dispatchModules({ + res, + modules, + params: { + history: prompts, + userChatInput: prompt.value + }, + stream + }); + + // save chat + if (typeof chatId === 'string') { + const { newChatId } = await saveChat({ + chatId, + modelId: appId, + prompts: [ + prompt, + { + _id: messages[messages.length - 1]._id, + obj: ChatRoleEnum.AI, + value: answerText, + responseData + } + ], + userId + }); + + if (newChatId) { + sseResponse({ + res, + event: sseResponseEventEnum.chatResponse, + data: JSON.stringify({ + newChatId + }) + }); + } + } + + if (stream) { + sseResponse({ + res, + event: sseResponseEventEnum.appStreamResponse, + data: JSON.stringify(responseData) + }); + res.end(); + } else { + res.json({ + data: responseData, + id: chatId || '', + model: '', + usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, + choices: [ + { + message: [{ role: 'assistant', content: answerText }], + finish_reason: 'stop', + index: 0 + } + ] + }); + } + } catch (err: any) { + if (stream) { + res.status(500); + sseErrRes(res, err); + res.end(); + } else { + jsonRes(res, { + code: 500, + error: err + }); + } + } +}); + +async function dispatchModules({ + res, + modules, + params = {}, + stream = false +}: { + res: NextApiResponse; + modules: AppModuleItemType[]; + params?: Record; + stream?: boolean; +}) { + let storeData: Record = {}; + let responseData: Record = {}; + let answerText = ''; + + function pushStore({ + isResponse = false, + answer, + data = {} + }: { + isResponse?: boolean; + answer?: string; + data?: Record; + }) { + if (isResponse) { + responseData = { + ...responseData, + ...data + }; + } + + if (answer) { + answerText += answer; + } + + storeData = { + ...storeData, + ...data + }; + } + function moduleInput(module: AppModuleItemType, data: Record = {}): Promise { + const checkInputFinish = () => { + return !module.inputs.find((item: any) => item.value === undefined); + }; + const updateInputValue = (key: string, value: any) => { + const index = module.inputs.findIndex((item: any) => item.key === key); + if (index === -1) return; + module.inputs[index].value = value; + }; + + return Promise.all( + Object.entries(data).map(([key, val]: any) => { + updateInputValue(key, val); + if (checkInputFinish()) { + return moduleRun(module); + } + }) + ); + } + function moduleOutput(module: AppModuleItemType, result: Record = {}): Promise { + return Promise.all( + module.outputs.map((item) => { + if (result[item.key] === undefined) return; + /* update output value */ + item.value = result[item.key]; + + pushStore({ + isResponse: item.response, + answer: item.answer ? item.value : '', + data: { + [item.key]: item.value + } + }); + + /* update target */ + return Promise.all( + item.targets.map((target: any) => { + // find module + const targetModule = modules.find((item) => item.moduleId === target.moduleId); + if (!targetModule) return; + return moduleInput(targetModule, { [target.key]: item.value }); + }) + ); + }) + ); + } + async function moduleRun(module: AppModuleItemType): Promise { + console.log('run=========', module.type, module.url); + + if (module.type === AppModuleItemTypeEnum.answer) { + pushStore({ + answer: module.inputs[0].value + }); + return AnswerResponse({ + res, + stream, + text: module.inputs.find((item) => item.key === SpecificInputEnum.answerText)?.value + }); + } + + if (module.type === AppModuleItemTypeEnum.switch) { + return moduleOutput(module, switchResponse(module)); + } + + if (module.type === AppModuleItemTypeEnum.http && module.url) { + // get fetch params + const inputParams: Record = {}; + module.inputs.forEach((item: any) => { + inputParams[item.key] = item.value; + }); + const data = { + stream, + ...module.body, + ...inputParams + }; + + // response data + const fetchRes = await moduleFetch({ + res, + url: module.url, + data + }); + + return moduleOutput(module, fetchRes); + } + } + + // 从填充 params 开始进入递归 + await Promise.all(modules.map((module) => moduleInput(module, params))); + + return { + responseData, + answerText + }; +} + +function AnswerResponse({ + res, + stream = false, + text = '' +}: { + res: NextApiResponse; + stream?: boolean; + text?: ''; +}) { + if (stream) { + return sseResponse({ + res, + event: sseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text + }) + }); + } + return text; +} +function switchResponse(module: any) { + const val = module?.inputs?.[0]?.value; + + if (val) { + return { true: 1 }; + } + return { false: 1 }; +} diff --git a/client/src/service/ai/openai.ts b/client/src/service/ai/openai.ts new file mode 100644 index 000000000..afd09e45d --- /dev/null +++ b/client/src/service/ai/openai.ts @@ -0,0 +1,25 @@ +import { Configuration, OpenAIApi } from 'openai'; + +export const getSystemOpenAiKey = () => { + return process.env.ONEAPI_KEY || ''; +}; + +export const getOpenAIApi = () => { + return new OpenAIApi( + new Configuration({ + basePath: process.env.ONEAPI_URL + }) + ); +}; + +/* openai axios config */ +export const axiosConfig = () => { + return { + baseURL: process.env.ONEAPI_URL, // 此处仅对非 npm 模块有效 + httpsAgent: global.httpsAgent, + headers: { + Authorization: `Bearer ${getSystemOpenAiKey()}`, + auth: process.env.OPENAI_BASE_URL_AUTH || '' + } + }; +}; diff --git a/client/src/service/api/request.ts b/client/src/service/api/request.ts index 757c18033..64a6304e1 100644 --- a/client/src/service/api/request.ts +++ b/client/src/service/api/request.ts @@ -1,122 +1,79 @@ -import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios'; +import { sseResponseEventEnum } from '@/constants/chat'; +import { getErrText } from '@/utils/tools'; +import { parseStreamChunk } from '@/utils/adapt'; +import { NextApiResponse } from 'next'; +import { sseResponse } from '../utils/tools'; -interface ConfigType { - headers?: { [key: string]: string }; - hold?: boolean; -} -interface ResponseDataType { - code: number; - message: string; - data: any; +interface Props { + res: NextApiResponse; // 用于流转发 + url: string; + data: Record; } +export const moduleFetch = ({ url, data, res }: Props) => + new Promise>(async (resolve, reject) => { + try { + const baseUrl = `http://localhost:3000/api`; + const requestUrl = url.startsWith('/') ? `${baseUrl}${url}` : url; + const response = await fetch(requestUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }); -/** - * 请求开始 - */ -function requestStart(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig { - if (config.headers) { - config.headers.rootkey = process.env.ROOT_KEY; - } + if (!response?.body) { + throw new Error('Request Error'); + } - return config; -} + const responseType = response.headers.get('content-type'); + if (responseType && responseType.includes('application/json')) { + const jsonResponse = await response.json(); + return resolve(jsonResponse?.data || {}); + } -/** - * 请求成功,检查请求头 - */ -function responseSuccess(response: AxiosResponse) { - return response; -} -/** - * 响应数据检查 - */ -function checkRes(data: ResponseDataType) { - if (data === undefined) { - return Promise.reject('服务器异常'); - } else if (data.code < 200 || data.code >= 400) { - return Promise.reject(data); - } - return data.data; -} + const reader = response.body?.getReader(); -/** - * 响应错误 - */ -function responseError(err: any) { - if (!err) { - return Promise.reject({ message: '未知错误' }); - } - if (typeof err === 'string') { - return Promise.reject({ message: err }); - } - return Promise.reject(err); -} + let chatResponse = {}; -/* 创建请求实例 */ -export const instance = axios.create({ - timeout: 60000, // 超时时间 - baseURL: `http://localhost:${process.env.PORT || 3000}/api`, - headers: { - rootkey: process.env.ROOT_KEY - } -}); + const read = async () => { + try { + const { done, value } = await reader.read(); + if (done) { + return resolve(chatResponse); + } + const chunkResponse = parseStreamChunk(value); -/* 请求拦截 */ -instance.interceptors.request.use(requestStart, (err) => Promise.reject(err)); -/* 响应拦截 */ -instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err)); - -function request(url: string, data: any, config: ConfigType, method: Method): any { - /* 去空 */ - for (const key in data) { - if (data[key] === null || data[key] === undefined) { - delete data[key]; + chunkResponse.forEach((item) => { + // parse json data + const data = (() => { + try { + return JSON.parse(item.data); + } catch (error) { + return {}; + } + })(); + if (item.event === sseResponseEventEnum.moduleFetchResponse) { + chatResponse = { + ...chatResponse, + ...data + }; + } else if (item.event === sseResponseEventEnum.answer && data?.choices?.[0]?.delta) { + sseResponse({ + res, + event: sseResponseEventEnum.answer, + data: JSON.stringify(data) + }); + } + }); + read(); + } catch (err: any) { + reject(getErrText(err, '请求异常')); + } + }; + read(); + } catch (err: any) { + console.log(err); + reject(getErrText(err, '请求异常')); } - } - - return instance - .request({ - url, - method, - data: method === 'GET' ? null : data, - params: method === 'GET' ? data : null, // get请求不携带data,params放在url上 - ...config // 用户自定义配置,可以覆盖前面的配置 - }) - .then((res) => checkRes(res.data)) - .catch((err) => responseError(err)); -} - -/** - * api请求方式 - * @param {String} url - * @param {Any} params - * @param {Object} config - * @returns - */ -export function GET( - url: string, - params = {}, - config: ConfigType = {} -): Promise { - return request(url, params, config, 'GET'); -} - -export function POST( - url: string, - data = {}, - config: ConfigType = {} -): Promise { - return request(url, data, config, 'POST'); -} - -export function PUT( - url: string, - data = {}, - config: ConfigType = {} -): Promise { - return request(url, data, config, 'PUT'); -} - -export function DELETE(url: string, config: ConfigType = {}): Promise { - return request(url, {}, config, 'DELETE'); -} + }); diff --git a/client/src/service/response.ts b/client/src/service/response.ts index e57ab5dd1..3afd74060 100644 --- a/client/src/service/response.ts +++ b/client/src/service/response.ts @@ -1,3 +1,4 @@ +import { sseResponseEventEnum } from '@/constants/chat'; import { NextApiResponse } from 'next'; import { openaiError, @@ -6,7 +7,7 @@ import { ERROR_RESPONSE, ERROR_ENUM } from './errorCode'; -import { clearCookie } from './utils/tools'; +import { clearCookie, sseResponse } from './utils/tools'; export interface ResponseType { code: number; @@ -61,3 +62,41 @@ export const jsonRes = ( data: data !== undefined ? data : null }); }; + +export const sseErrRes = (res: NextApiResponse, error: any) => { + const errResponseKey = typeof error === 'string' ? error : error?.message; + + // Specified error + if (ERROR_RESPONSE[errResponseKey]) { + // login is expired + if (errResponseKey === ERROR_ENUM.unAuthorization) { + clearCookie(res); + } + + return sseResponse({ + res, + event: sseResponseEventEnum.error, + data: JSON.stringify(ERROR_RESPONSE[errResponseKey]) + }); + } + + let msg = error?.message || '请求错误'; + if (typeof error === 'string') { + msg = error; + } else if (proxyError[error?.code]) { + msg = '接口连接异常'; + } else if (error?.response?.data?.error?.message) { + msg = error?.response?.data?.error?.message; + } else if (openaiAccountError[error?.response?.data?.error?.code]) { + msg = openaiAccountError[error?.response?.data?.error?.code]; + } else if (openaiError[error?.response?.statusText]) { + msg = openaiError[error.response.statusText]; + } + console.log(error); + + sseResponse({ + res, + event: sseResponseEventEnum.error, + data: JSON.stringify({ message: msg }) + }); +}; diff --git a/client/src/service/utils/tools.ts b/client/src/service/utils/tools.ts index ec9f5f7bd..f0589b6f5 100644 --- a/client/src/service/utils/tools.ts +++ b/client/src/service/utils/tools.ts @@ -79,7 +79,7 @@ export const sseResponse = ({ data }: { res: NextApiResponse; - event?: `${sseResponseEventEnum}`; + event?: string; data: string; }) => { event && res.write(`event: ${event}\n`); diff --git a/client/src/types/app.d.ts b/client/src/types/app.d.ts new file mode 100644 index 000000000..ac588ac4f --- /dev/null +++ b/client/src/types/app.d.ts @@ -0,0 +1,69 @@ +import { AppModuleItemTypeEnum, ModulesInputItemTypeEnum } from '../constants/app'; + +/* input item */ +export type ModuleItemCommonType = { + key: string; // 字段名 + formType: `${ModuleInputItemTypeEnum}`; + label: string; + description?: string; + placeholder?: string; + max?: number; + min?: number; + default?: any; + enum?: { label: string; value: any }[]; +}; + +export type ModuleItemOutputItemType = { + key: string; + targets: { moduleId: string; key: string }[]; +}; + +export type ModuleItemType = { + moduleId: string; + avatar: string; + name: string; + description: string; + url: string; + body: ModuleItemCommonType[]; + inputs: ModuleItemCommonType[]; + outputs: ModuleItemOutputItemType[]; +}; + +/* input item */ +type FormItemCommonType = { + key: string; // 字段名 + label: string; + description: string; + formType: `${ModulesInputItemTypeEnum}`; +}; + +/* agent */ +/* question classify */ +export type ClassifyQuestionAgentItemType = { + desc: string; + key: string; +}; + +/* app module */ +export type AppModuleItemType = { + moduleId: string; + type: `${AppModuleItemTypeEnum}`; + url?: string; + body: Record; + inputs: { key: string; value: any }[]; + outputs: { + key: string; + value?: any; + response?: boolean; + answer?: boolean; // json response + targets: { + moduleId: string; + key: string; + }[]; + }[]; +}; + +export type AppItemType = { + id: string; + modules: AppModuleItemType[]; +}; diff --git a/client/src/types/chat.d.ts b/client/src/types/chat.d.ts index c57939c28..9556ca825 100644 --- a/client/src/types/chat.d.ts +++ b/client/src/types/chat.d.ts @@ -11,6 +11,7 @@ export type ChatItemType = { quoteLen?: number; quote?: QuoteItemType[]; systemPrompt?: string; + [key: string]: any; }; export type ChatSiteItemType = {