Commit 4c54e182 by archer

feat: app module

parent 7e6272ca
...@@ -12,7 +12,7 @@ export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) ...@@ -12,7 +12,7 @@ export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps)
new Promise<ChatResponseType & { responseText: string; errMsg: string }>( new Promise<ChatResponseType & { responseText: string; errMsg: string }>(
async (resolve, reject) => { async (resolve, reject) => {
try { try {
const response = await window.fetch('/api/openapi/v1/chat/completions', { const response = await window.fetch('/api/openapi/v1/chat/test', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
...@@ -74,8 +74,9 @@ export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) ...@@ -74,8 +74,9 @@ export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps)
responseText += answer; responseText += answer;
} else if (item.event === sseResponseEventEnum.chatResponse) { } else if (item.event === sseResponseEventEnum.chatResponse) {
const chatResponse = data as ChatResponseType; const chatResponse = data as ChatResponseType;
newChatId = chatResponse.newChatId; newChatId =
quoteLen = chatResponse.quoteLen || 0; chatResponse.newChatId !== undefined ? chatResponse.newChatId : newChatId;
quoteLen = chatResponse.quoteLen !== undefined ? chatResponse.quoteLen : quoteLen;
} else if (item.event === sseResponseEventEnum.error) { } else if (item.event === sseResponseEventEnum.error) {
errMsg = getErrText(data, '流响应错误'); errMsg = getErrText(data, '流响应错误');
} }
......
export enum sseResponseEventEnum { export enum sseResponseEventEnum {
error = 'error', error = 'error',
answer = 'answer', answer = 'answer',
chatResponse = 'chatResponse' chatResponse = 'chatResponse', //
appStreamResponse = 'appStreamResponse', // sse response request
moduleFetchResponse = 'moduleFetchResponse' // http module sse response
} }
export enum ChatRoleEnum { export enum ChatRoleEnum {
......
...@@ -47,7 +47,7 @@ export async function saveChat({ ...@@ -47,7 +47,7 @@ export async function saveChat({
modelId, modelId,
prompts, prompts,
userId userId
}: Props & { newChatId?: Types.ObjectId; userId: string }) { }: Props & { newChatId?: Types.ObjectId; userId: string }): Promise<{ newChatId: string }> {
await connectToDatabase(); await connectToDatabase();
const { model } = await authModel({ modelId, userId, authOwner: false }); const { model } = await authModel({ modelId, userId, authOwner: false });
...@@ -104,6 +104,7 @@ export async function saveChat({ ...@@ -104,6 +104,7 @@ export async function saveChat({
]); ]);
return { return {
...response // @ts-ignore
newChatId: response?.newChatId || ''
}; };
} }
// 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
};
}
// 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;
}
// 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
};
}
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<any>) {
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<Response>(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<Response> {
// 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}` : ''
};
}
export type Props = {
url: string;
body: Record<string, any>;
};
...@@ -81,3 +81,35 @@ export async function openaiEmbedding({ ...@@ -81,3 +81,35 @@ export async function openaiEmbedding({
return result.vectors; 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;
}
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<string, any>;
stream?: boolean;
}) {
let storeData: Record<string, any> = {};
let responseData: Record<string, any> = {};
let answerText = '';
function pushStore({
isResponse = false,
answer,
data = {}
}: {
isResponse?: boolean;
answer?: string;
data?: Record<string, any>;
}) {
if (isResponse) {
responseData = {
...responseData,
...data
};
}
if (answer) {
answerText += answer;
}
storeData = {
...storeData,
...data
};
}
function moduleInput(module: AppModuleItemType, data: Record<string, any> = {}): Promise<any> {
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<string, any> = {}): Promise<any> {
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<any> {
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<string, any> = {};
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 };
}
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 || ''
}
};
};
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 { interface Props {
headers?: { [key: string]: string }; res: NextApiResponse; // 用于流转发
hold?: boolean; url: string;
data: Record<string, any>;
} }
interface ResponseDataType { export const moduleFetch = ({ url, data, res }: Props) =>
code: number; new Promise<Record<string, any>>(async (resolve, reject) => {
message: string; try {
data: any; 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'
function requestStart(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig { },
if (config.headers) { body: JSON.stringify(data)
config.headers.rootkey = process.env.ROOT_KEY; });
}
return config; if (!response?.body) {
} throw new Error('Request Error');
}
/** const responseType = response.headers.get('content-type');
* 请求成功,检查请求头 if (responseType && responseType.includes('application/json')) {
*/ const jsonResponse = await response.json();
function responseSuccess(response: AxiosResponse<ResponseDataType>) { return resolve(jsonResponse?.data || {});
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 () => {
instance.interceptors.request.use(requestStart, (err) => Promise.reject(err)); try {
/* 响应拦截 */ const { done, value } = await reader.read();
instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err)); if (done) {
return resolve(chatResponse);
}
const chunkResponse = parseStreamChunk(value);
function request(url: string, data: any, config: ConfigType, method: Method): any { chunkResponse.forEach((item) => {
/* 去空 */ // parse json data
for (const key in data) { const data = (() => {
if (data[key] === null || data[key] === undefined) { try {
delete data[key]; 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<T = { data: any }>(
url: string,
params = {},
config: ConfigType = {}
): Promise<T> {
return request(url, params, config, 'GET');
}
export function POST<T = { data: any }>(
url: string,
data = {},
config: ConfigType = {}
): Promise<T> {
return request(url, data, config, 'POST');
}
export function PUT<T = { data: any }>(
url: string,
data = {},
config: ConfigType = {}
): Promise<T> {
return request(url, data, config, 'PUT');
}
export function DELETE<T = { data: any }>(url: string, config: ConfigType = {}): Promise<T> {
return request(url, {}, config, 'DELETE');
}
import { sseResponseEventEnum } from '@/constants/chat';
import { NextApiResponse } from 'next'; import { NextApiResponse } from 'next';
import { import {
openaiError, openaiError,
...@@ -6,7 +7,7 @@ import { ...@@ -6,7 +7,7 @@ import {
ERROR_RESPONSE, ERROR_RESPONSE,
ERROR_ENUM ERROR_ENUM
} from './errorCode'; } from './errorCode';
import { clearCookie } from './utils/tools'; import { clearCookie, sseResponse } from './utils/tools';
export interface ResponseType<T = any> { export interface ResponseType<T = any> {
code: number; code: number;
...@@ -61,3 +62,41 @@ export const jsonRes = <T = any>( ...@@ -61,3 +62,41 @@ export const jsonRes = <T = any>(
data: data !== undefined ? data : null 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 })
});
};
...@@ -79,7 +79,7 @@ export const sseResponse = ({ ...@@ -79,7 +79,7 @@ export const sseResponse = ({
data data
}: { }: {
res: NextApiResponse; res: NextApiResponse;
event?: `${sseResponseEventEnum}`; event?: string;
data: string; data: string;
}) => { }) => {
event && res.write(`event: ${event}\n`); event && res.write(`event: ${event}\n`);
......
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<string, any>;
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[];
};
...@@ -11,6 +11,7 @@ export type ChatItemType = { ...@@ -11,6 +11,7 @@ export type ChatItemType = {
quoteLen?: number; quoteLen?: number;
quote?: QuoteItemType[]; quote?: QuoteItemType[];
systemPrompt?: string; systemPrompt?: string;
[key: string]: any;
}; };
export type ChatSiteItemType = { export type ChatSiteItemType = {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment