337 lines
9.4 KiB
TypeScript
337 lines
9.4 KiB
TypeScript
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
import { connectToDatabase } from '@/service/mongo';
|
|
import { authUser, authModel, getApiKey, authShareChat } from '@/service/utils/auth';
|
|
import { modelServiceToolMap, V2_StreamResponse } from '@/service/utils/chat';
|
|
import { jsonRes } from '@/service/response';
|
|
import { ChatModelMap } from '@/constants/model';
|
|
import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
|
|
import { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
|
import { withNextCors } from '@/service/utils/tools';
|
|
import { BillTypeEnum } from '@/constants/user';
|
|
import { appKbSearch } from '../../../openapi/kb/appKbSearch';
|
|
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 { Types } from 'mongoose';
|
|
import { sensitiveCheck } from '../../text/sensitiveCheck';
|
|
|
|
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[];
|
|
};
|
|
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;
|
|
let step = 0;
|
|
|
|
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');
|
|
}
|
|
|
|
// auth app permission
|
|
const { model, showModelDetail } = await authModel({
|
|
userId,
|
|
modelId: appId,
|
|
authOwner: false,
|
|
reserveDetail: true
|
|
});
|
|
|
|
const showAppDetail = !shareId && showModelDetail;
|
|
|
|
/* get api key */
|
|
const { systemAuthKey: apiKey, userOpenAiKey } = await getApiKey({
|
|
model: model.chat.chatModel,
|
|
userId,
|
|
mustPay: authType !== 'token'
|
|
});
|
|
|
|
// get history
|
|
const { history } = await getChatHistory({ chatId, userId });
|
|
const prompts = history.concat(gptMessage2ChatType(messages));
|
|
// adapt fastgpt web
|
|
if (prompts[prompts.length - 1].obj === 'AI') {
|
|
prompts.pop();
|
|
}
|
|
// user question
|
|
const prompt = prompts[prompts.length - 1];
|
|
|
|
const {
|
|
rawSearch = [],
|
|
userSystemPrompt = [],
|
|
userLimitPrompt = [],
|
|
quotePrompt = []
|
|
} = await (async () => {
|
|
// 使用了知识库搜索
|
|
if (model.chat.relatedKbs?.length > 0) {
|
|
const { rawSearch, quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
|
|
model,
|
|
userId,
|
|
fixedQuote: history[history.length - 1]?.quote,
|
|
prompt,
|
|
similarity: model.chat.searchSimilarity,
|
|
limit: model.chat.searchLimit
|
|
});
|
|
|
|
return {
|
|
rawSearch,
|
|
userSystemPrompt,
|
|
userLimitPrompt,
|
|
quotePrompt: [quotePrompt]
|
|
};
|
|
}
|
|
return {
|
|
userSystemPrompt: model.chat.systemPrompt
|
|
? [
|
|
{
|
|
obj: ChatRoleEnum.System,
|
|
value: model.chat.systemPrompt
|
|
}
|
|
]
|
|
: [],
|
|
userLimitPrompt: model.chat.limitPrompt
|
|
? [
|
|
{
|
|
obj: ChatRoleEnum.Human,
|
|
value: model.chat.limitPrompt
|
|
}
|
|
]
|
|
: []
|
|
};
|
|
})();
|
|
|
|
// search result is empty
|
|
if (model.chat.relatedKbs?.length > 0 && !quotePrompt[0]?.value && model.chat.searchEmptyText) {
|
|
const response = model.chat.searchEmptyText;
|
|
if (stream) {
|
|
sseResponse({
|
|
res,
|
|
event: sseResponseEventEnum.answer,
|
|
data: textAdaptGptResponse({
|
|
text: response,
|
|
model: model.chat.chatModel,
|
|
finish_reason: 'stop'
|
|
})
|
|
});
|
|
return res.end();
|
|
} else {
|
|
return res.json({
|
|
id: chatId || '',
|
|
model: model.chat.chatModel,
|
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
choices: [
|
|
{ message: { role: 'assistant', content: response }, finish_reason: 'stop', index: 0 }
|
|
]
|
|
});
|
|
}
|
|
}
|
|
|
|
// api messages. [quote,context,systemPrompt,question]
|
|
const completePrompts = [
|
|
...quotePrompt,
|
|
...userSystemPrompt,
|
|
...prompts.slice(0, -1),
|
|
...userLimitPrompt,
|
|
prompt
|
|
];
|
|
// chat temperature
|
|
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
|
// FastGpt temperature range: 1~10
|
|
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
|
|
2
|
|
);
|
|
|
|
await sensitiveCheck({
|
|
input: `${userSystemPrompt[0]?.value}\n${userLimitPrompt[0]?.value}\n${prompt.value}`
|
|
});
|
|
|
|
// start model api. responseText and totalTokens: valid only if stream = false
|
|
const { streamResponse, responseMessages, responseText, totalTokens } =
|
|
await modelServiceToolMap.chatCompletion({
|
|
model: model.chat.chatModel,
|
|
apiKey: userOpenAiKey || apiKey,
|
|
temperature: +temperature,
|
|
maxToken: model.chat.maxToken,
|
|
messages: completePrompts,
|
|
stream,
|
|
res
|
|
});
|
|
|
|
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
|
|
|
if (res.closed) return res.end();
|
|
|
|
// create a chatId
|
|
const newChatId = chatId === '' ? new Types.ObjectId() : undefined;
|
|
|
|
// response answer
|
|
const {
|
|
textLen = 0,
|
|
answer = responseText,
|
|
tokens = totalTokens
|
|
} = await (async () => {
|
|
if (stream) {
|
|
// 创建响应流
|
|
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Transfer-Encoding', 'chunked');
|
|
res.setHeader('X-Accel-Buffering', 'no');
|
|
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
step = 1;
|
|
|
|
try {
|
|
// response newChatId and quota
|
|
sseResponse({
|
|
res,
|
|
event: sseResponseEventEnum.chatResponse,
|
|
data: JSON.stringify({
|
|
newChatId,
|
|
quoteLen: rawSearch.length
|
|
})
|
|
});
|
|
// response answer
|
|
const { finishMessages, totalTokens, responseContent } = await V2_StreamResponse({
|
|
model: model.chat.chatModel,
|
|
res,
|
|
chatResponse: streamResponse,
|
|
prompts: responseMessages
|
|
});
|
|
return {
|
|
answer: responseContent,
|
|
textLen: finishMessages.map((item) => item.value).join('').length,
|
|
tokens: totalTokens
|
|
};
|
|
} catch (error) {
|
|
return Promise.reject(error);
|
|
}
|
|
} else {
|
|
return {
|
|
textLen: responseMessages.map((item) => item.value).join('').length
|
|
};
|
|
}
|
|
})();
|
|
|
|
// save chat history
|
|
if (typeof chatId === 'string') {
|
|
await saveChat({
|
|
newChatId,
|
|
chatId,
|
|
modelId: appId,
|
|
prompts: [
|
|
prompt,
|
|
{
|
|
_id: messages[messages.length - 1]._id,
|
|
obj: ChatRoleEnum.AI,
|
|
value: answer,
|
|
...(showAppDetail
|
|
? {
|
|
quote: rawSearch,
|
|
systemPrompt: `${userSystemPrompt[0]?.value}\n\n${userLimitPrompt[0]?.value}`
|
|
}
|
|
: {})
|
|
}
|
|
],
|
|
userId
|
|
});
|
|
}
|
|
|
|
// close response
|
|
if (stream) {
|
|
res.end();
|
|
} else {
|
|
res.json({
|
|
...(showAppDetail
|
|
? {
|
|
rawSearch
|
|
}
|
|
: {}),
|
|
newChatId,
|
|
id: chatId || '',
|
|
model: model.chat.chatModel,
|
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: tokens },
|
|
choices: [
|
|
{ message: { role: 'assistant', content: answer }, finish_reason: 'stop', index: 0 }
|
|
]
|
|
});
|
|
}
|
|
|
|
pushChatBill({
|
|
isPay: !userOpenAiKey,
|
|
chatModel: model.chat.chatModel,
|
|
userId,
|
|
textLen,
|
|
tokens,
|
|
type: authType === 'apikey' ? BillTypeEnum.openapiChat : BillTypeEnum.chat
|
|
});
|
|
shareId &&
|
|
updateShareChatBill({
|
|
shareId,
|
|
tokens
|
|
});
|
|
} catch (err: any) {
|
|
res.status(500);
|
|
if (step === 1) {
|
|
sseResponse({
|
|
res,
|
|
event: sseResponseEventEnum.error,
|
|
data: JSON.stringify(err)
|
|
});
|
|
res.end();
|
|
} else {
|
|
jsonRes(res, {
|
|
code: 500,
|
|
error: err
|
|
});
|
|
}
|
|
}
|
|
});
|