310 lines
7.7 KiB
TypeScript
310 lines
7.7 KiB
TypeScript
import type { NextApiResponse } from 'next';
|
|
import { sseResponse } from '@/service/utils/tools';
|
|
import { OpenAiChatEnum } from '@/constants/model';
|
|
import { adaptChatItem_openAI, countOpenAIToken } from '@/utils/plugin/openai';
|
|
import { modelToolMap } from '@/utils/plugin';
|
|
import { ChatContextFilter } from '@/service/utils/chat/index';
|
|
import type { ChatItemType, QuoteItemType } from '@/types/chat';
|
|
import type { ChatHistoryItemResType } from '@/types/chat';
|
|
import { ChatModuleEnum, ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
|
|
import { SSEParseData, parseStreamChunk } from '@/utils/sse';
|
|
import { textAdaptGptResponse } from '@/utils/adapt';
|
|
import { getOpenAIApi, axiosConfig } from '@/service/ai/openai';
|
|
import { TaskResponseKeyEnum } from '@/constants/chat';
|
|
import { getChatModel } from '@/service/utils/data';
|
|
import { countModelPrice } from '@/service/events/pushBill';
|
|
import { ChatModelItemType } from '@/types/model';
|
|
|
|
export type ChatProps = {
|
|
res: NextApiResponse;
|
|
model: `${OpenAiChatEnum}`;
|
|
temperature?: number;
|
|
maxToken?: number;
|
|
history?: ChatItemType[];
|
|
userChatInput: string;
|
|
stream?: boolean;
|
|
quoteQA?: QuoteItemType[];
|
|
systemPrompt?: string;
|
|
limitPrompt?: string;
|
|
};
|
|
export type ChatResponse = {
|
|
[TaskResponseKeyEnum.answerText]: string;
|
|
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
|
|
};
|
|
|
|
/* request openai chat */
|
|
export const dispatchChatCompletion = async (props: Record<string, any>): Promise<ChatResponse> => {
|
|
let {
|
|
res,
|
|
model,
|
|
temperature = 0,
|
|
maxToken = 4000,
|
|
stream = false,
|
|
history = [],
|
|
quoteQA = [],
|
|
userChatInput,
|
|
systemPrompt = '',
|
|
limitPrompt = ''
|
|
} = props as ChatProps;
|
|
|
|
// temperature adapt
|
|
const modelConstantsData = getChatModel(model);
|
|
|
|
if (!modelConstantsData) {
|
|
return Promise.reject('The chat model is undefined, you need to select a chat model.');
|
|
}
|
|
|
|
const { filterQuoteQA, quotePrompt } = filterQuote({
|
|
quoteQA,
|
|
model: modelConstantsData
|
|
});
|
|
|
|
const { messages, filterMessages } = getChatMessages({
|
|
model: modelConstantsData,
|
|
history,
|
|
quotePrompt,
|
|
userChatInput,
|
|
systemPrompt,
|
|
limitPrompt
|
|
});
|
|
const { max_tokens } = getMaxTokens({
|
|
model: modelConstantsData,
|
|
maxToken,
|
|
filterMessages
|
|
});
|
|
// console.log(messages);
|
|
|
|
// FastGpt temperature range: 1~10
|
|
temperature = +(modelConstantsData.maxTemperature * (temperature / 10)).toFixed(2);
|
|
const chatAPI = getOpenAIApi();
|
|
|
|
const response = await chatAPI.createChatCompletion(
|
|
{
|
|
model,
|
|
temperature: Number(temperature || 0),
|
|
max_tokens,
|
|
messages,
|
|
// frequency_penalty: 0.5, // 越大,重复内容越少
|
|
// presence_penalty: -0.5, // 越大,越容易出现新内容
|
|
stream
|
|
},
|
|
{
|
|
timeout: stream ? 60000 : 480000,
|
|
responseType: stream ? 'stream' : 'json',
|
|
...axiosConfig()
|
|
}
|
|
);
|
|
|
|
const { answerText, totalTokens, completeMessages } = await (async () => {
|
|
if (stream) {
|
|
// sse response
|
|
const { answer } = await streamResponse({ res, response });
|
|
// count tokens
|
|
const completeMessages = filterMessages.concat({
|
|
obj: ChatRoleEnum.AI,
|
|
value: answer
|
|
});
|
|
|
|
const totalTokens = countOpenAIToken({
|
|
messages: completeMessages
|
|
});
|
|
|
|
return {
|
|
answerText: answer,
|
|
totalTokens,
|
|
completeMessages
|
|
};
|
|
} else {
|
|
const answer = stream ? '' : response.data.choices?.[0].message?.content || '';
|
|
const totalTokens = stream ? 0 : response.data.usage?.total_tokens || 0;
|
|
|
|
const completeMessages = filterMessages.concat({
|
|
obj: ChatRoleEnum.AI,
|
|
value: answer
|
|
});
|
|
|
|
return {
|
|
answerText: answer,
|
|
totalTokens,
|
|
completeMessages
|
|
};
|
|
}
|
|
})();
|
|
|
|
return {
|
|
[TaskResponseKeyEnum.answerText]: answerText,
|
|
[TaskResponseKeyEnum.responseData]: {
|
|
moduleName: ChatModuleEnum.AIChat,
|
|
price: countModelPrice({ model, tokens: totalTokens }),
|
|
model: modelConstantsData.name,
|
|
tokens: totalTokens,
|
|
question: userChatInput,
|
|
answer: answerText,
|
|
maxToken,
|
|
quoteList: filterQuoteQA,
|
|
completeMessages
|
|
}
|
|
};
|
|
};
|
|
|
|
function filterQuote({
|
|
quoteQA = [],
|
|
model
|
|
}: {
|
|
quoteQA: ChatProps['quoteQA'];
|
|
model: ChatModelItemType;
|
|
}) {
|
|
const sliceResult = modelToolMap.tokenSlice({
|
|
model: model.model,
|
|
maxToken: model.quoteMaxToken,
|
|
messages: quoteQA.map((item, i) => ({
|
|
obj: ChatRoleEnum.System,
|
|
value: `${i + 1}. [${item.q}\n${item.a}]`
|
|
}))
|
|
});
|
|
|
|
// slice filterSearch
|
|
const filterQuoteQA = quoteQA.slice(0, sliceResult.length);
|
|
|
|
const quotePrompt =
|
|
filterQuoteQA.length > 0
|
|
? `下面是知识库内容:
|
|
${filterQuoteQA.map((item, i) => `${i + 1}. [${item.q}\n${item.a}]`).join('\n')}
|
|
`
|
|
: '';
|
|
|
|
return {
|
|
filterQuoteQA,
|
|
quotePrompt
|
|
};
|
|
}
|
|
function getChatMessages({
|
|
quotePrompt,
|
|
history = [],
|
|
systemPrompt,
|
|
limitPrompt,
|
|
userChatInput,
|
|
model
|
|
}: {
|
|
quotePrompt: string;
|
|
history: ChatProps['history'];
|
|
systemPrompt: string;
|
|
limitPrompt: string;
|
|
userChatInput: string;
|
|
model: ChatModelItemType;
|
|
}) {
|
|
const limitText = (() => {
|
|
if (limitPrompt) return limitPrompt;
|
|
if (quotePrompt && !limitPrompt) {
|
|
return '根据知识库内容回答问题,仅回复知识库提供的内容,不要对知识库内容做补充说明。';
|
|
}
|
|
return '';
|
|
})();
|
|
|
|
const messages: ChatItemType[] = [
|
|
...(quotePrompt
|
|
? [
|
|
{
|
|
obj: ChatRoleEnum.System,
|
|
value: quotePrompt
|
|
}
|
|
]
|
|
: []),
|
|
...(systemPrompt
|
|
? [
|
|
{
|
|
obj: ChatRoleEnum.System,
|
|
value: systemPrompt
|
|
}
|
|
]
|
|
: []),
|
|
...history,
|
|
...(limitText
|
|
? [
|
|
{
|
|
obj: ChatRoleEnum.System,
|
|
value: limitText
|
|
}
|
|
]
|
|
: []),
|
|
{
|
|
obj: ChatRoleEnum.Human,
|
|
value: userChatInput
|
|
}
|
|
];
|
|
|
|
const filterMessages = ChatContextFilter({
|
|
model: model.model,
|
|
prompts: messages,
|
|
maxTokens: Math.ceil(model.contextMaxToken - 300) // filter token. not response maxToken
|
|
});
|
|
|
|
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
|
|
|
|
return {
|
|
messages: adaptMessages,
|
|
filterMessages
|
|
};
|
|
}
|
|
function getMaxTokens({
|
|
maxToken,
|
|
model,
|
|
filterMessages = []
|
|
}: {
|
|
maxToken: number;
|
|
model: ChatModelItemType;
|
|
filterMessages: ChatProps['history'];
|
|
}) {
|
|
const tokensLimit = model.contextMaxToken;
|
|
/* count response max token */
|
|
const promptsToken = modelToolMap.countTokens({
|
|
model: model.model,
|
|
messages: filterMessages
|
|
});
|
|
maxToken = maxToken + promptsToken > tokensLimit ? tokensLimit - promptsToken : maxToken;
|
|
|
|
return {
|
|
max_tokens: maxToken
|
|
};
|
|
}
|
|
|
|
async function streamResponse({ res, response }: { res: NextApiResponse; response: any }) {
|
|
let answer = '';
|
|
let error: any = null;
|
|
const parseData = new SSEParseData();
|
|
|
|
try {
|
|
for await (const chunk of response.data as any) {
|
|
if (res.closed) break;
|
|
const parse = parseStreamChunk(chunk);
|
|
parse.forEach((item) => {
|
|
const { data } = parseData.parse(item);
|
|
if (!data || data === '[DONE]') return;
|
|
|
|
const content: string = data?.choices?.[0].delta.content || '';
|
|
error = data.error;
|
|
answer += content;
|
|
|
|
sseResponse({
|
|
res,
|
|
event: sseResponseEventEnum.answer,
|
|
data: textAdaptGptResponse({
|
|
text: content
|
|
})
|
|
});
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.log('pipe error', error);
|
|
}
|
|
|
|
if (error) {
|
|
console.log(error);
|
|
return Promise.reject(error);
|
|
}
|
|
|
|
return {
|
|
answer
|
|
};
|
|
}
|