390 lines
9.7 KiB
TypeScript
390 lines
9.7 KiB
TypeScript
import type { NextApiResponse } from 'next';
|
||
import { sseResponse } from '@/service/utils/tools';
|
||
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 { getAIChatApi, 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';
|
||
import { UserModelSchema } from '@/types/mongoSchema';
|
||
import { textCensor } from '@/api/service/plugins';
|
||
import { ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||
import { AppModuleItemType } from '@/types/app';
|
||
|
||
export type ChatProps = {
|
||
res: NextApiResponse;
|
||
model: string;
|
||
temperature?: number;
|
||
maxToken?: number;
|
||
history?: ChatItemType[];
|
||
userChatInput: string;
|
||
stream?: boolean;
|
||
detail?: boolean;
|
||
quoteQA?: QuoteItemType[];
|
||
systemPrompt?: string;
|
||
limitPrompt?: string;
|
||
userOpenaiAccount: UserModelSchema['openaiAccount'];
|
||
outputs: AppModuleItemType['outputs'];
|
||
};
|
||
export type ChatResponse = {
|
||
[TaskResponseKeyEnum.answerText]: string;
|
||
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
|
||
finish: boolean;
|
||
};
|
||
|
||
/* request openai chat */
|
||
export const dispatchChatCompletion = async (props: Record<string, any>): Promise<ChatResponse> => {
|
||
let {
|
||
res,
|
||
model = global.chatModels[0]?.model,
|
||
temperature = 0,
|
||
maxToken = 4000,
|
||
stream = false,
|
||
detail = false,
|
||
history = [],
|
||
quoteQA = [],
|
||
userChatInput,
|
||
systemPrompt = '',
|
||
limitPrompt = '',
|
||
userOpenaiAccount,
|
||
outputs
|
||
} = props as ChatProps;
|
||
if (!userChatInput) {
|
||
return Promise.reject('Question is empty');
|
||
}
|
||
|
||
// 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, hasQuoteOutput } = filterQuote({
|
||
quoteQA,
|
||
model: modelConstantsData
|
||
});
|
||
|
||
if (modelConstantsData.censor) {
|
||
await textCensor({
|
||
text: `${systemPrompt}
|
||
${quotePrompt}
|
||
${limitPrompt}
|
||
${userChatInput}
|
||
`
|
||
});
|
||
}
|
||
|
||
const { messages, filterMessages } = getChatMessages({
|
||
model: modelConstantsData,
|
||
history,
|
||
quotePrompt,
|
||
userChatInput,
|
||
systemPrompt,
|
||
limitPrompt,
|
||
hasQuoteOutput
|
||
});
|
||
const { max_tokens } = getMaxTokens({
|
||
model: modelConstantsData,
|
||
maxToken,
|
||
filterMessages
|
||
});
|
||
// console.log(messages);
|
||
|
||
// FastGPT temperature range: 1~10
|
||
temperature = +(modelConstantsData.maxTemperature * (temperature / 10)).toFixed(2);
|
||
temperature = Math.max(temperature, 0.01);
|
||
const chatAPI = getAIChatApi(userOpenaiAccount);
|
||
|
||
const response = await chatAPI.createChatCompletion(
|
||
{
|
||
model,
|
||
temperature,
|
||
max_tokens,
|
||
messages: [
|
||
...(modelConstantsData.defaultSystem
|
||
? [
|
||
{
|
||
role: ChatCompletionRequestMessageRoleEnum.System,
|
||
content: modelConstantsData.defaultSystem
|
||
}
|
||
]
|
||
: []),
|
||
...messages
|
||
],
|
||
// frequency_penalty: 0.5, // 越大,重复内容越少
|
||
// presence_penalty: -0.5, // 越大,越容易出现新内容
|
||
stream
|
||
},
|
||
{
|
||
timeout: stream ? 120000 : 480000,
|
||
responseType: stream ? 'stream' : 'json',
|
||
...axiosConfig(userOpenaiAccount)
|
||
}
|
||
);
|
||
|
||
const { answerText, totalTokens, completeMessages } = await (async () => {
|
||
if (stream) {
|
||
// sse response
|
||
const { answer } = await streamResponse({
|
||
res,
|
||
detail,
|
||
response
|
||
});
|
||
// count tokens
|
||
const completeMessages = filterMessages.concat({
|
||
obj: ChatRoleEnum.AI,
|
||
value: answer
|
||
});
|
||
|
||
const totalTokens = countOpenAIToken({
|
||
messages: completeMessages
|
||
});
|
||
|
||
targetResponse({ res, detail, outputs });
|
||
|
||
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: userOpenaiAccount?.key ? 0 : countModelPrice({ model, tokens: totalTokens }),
|
||
model: modelConstantsData.name,
|
||
tokens: totalTokens,
|
||
question: userChatInput,
|
||
answer: answerText,
|
||
maxToken: max_tokens,
|
||
quoteList: filterQuoteQA,
|
||
completeMessages
|
||
},
|
||
finish: true
|
||
};
|
||
};
|
||
|
||
function filterQuote({
|
||
quoteQA = [],
|
||
model
|
||
}: {
|
||
quoteQA: ChatProps['quoteQA'];
|
||
model: ChatModelItemType;
|
||
}) {
|
||
const sliceResult = modelToolMap.tokenSlice({
|
||
maxToken: model.quoteMaxToken,
|
||
messages: quoteQA.map((item) => ({
|
||
obj: ChatRoleEnum.System,
|
||
value: item.a ? `${item.q}\n${item.a}` : item.q
|
||
}))
|
||
});
|
||
|
||
// slice filterSearch
|
||
const filterQuoteQA = quoteQA.slice(0, sliceResult.length);
|
||
|
||
const quotePrompt =
|
||
filterQuoteQA.length > 0
|
||
? `"""${filterQuoteQA
|
||
.map((item) =>
|
||
item.a ? `{instruction:"${item.q}",output:"${item.a}"}` : `{instruction:"${item.q}"}`
|
||
)
|
||
.join('\n')}"""`
|
||
: '';
|
||
|
||
return {
|
||
filterQuoteQA,
|
||
quotePrompt,
|
||
hasQuoteOutput: !!filterQuoteQA.find((item) => item.a)
|
||
};
|
||
}
|
||
function getChatMessages({
|
||
quotePrompt,
|
||
history = [],
|
||
systemPrompt,
|
||
limitPrompt,
|
||
userChatInput,
|
||
model,
|
||
hasQuoteOutput
|
||
}: {
|
||
quotePrompt: string;
|
||
history: ChatProps['history'];
|
||
systemPrompt: string;
|
||
limitPrompt: string;
|
||
userChatInput: string;
|
||
model: ChatModelItemType;
|
||
hasQuoteOutput: boolean;
|
||
}) {
|
||
const limitText = (() => {
|
||
if (!quotePrompt) {
|
||
return limitPrompt;
|
||
}
|
||
const defaultPrompt = `三引号引用的内容是我提供给你的知识,它们拥有最高优先级。instruction 是相关介绍${
|
||
hasQuoteOutput ? ',output 是预期回答或补充' : ''
|
||
},使用引用内容来回答我下面的问题。`;
|
||
if (limitPrompt) {
|
||
return `${defaultPrompt}${limitPrompt}`;
|
||
}
|
||
return `${defaultPrompt}\n回答内容限制:你仅回答三引号中提及的内容,下面我提出的问题与引用内容无关时,你可以直接回复: "你的问题没有在知识库中体现"`;
|
||
})();
|
||
|
||
const messages: ChatItemType[] = [
|
||
...(systemPrompt
|
||
? [
|
||
{
|
||
obj: ChatRoleEnum.System,
|
||
value: systemPrompt
|
||
}
|
||
]
|
||
: []),
|
||
...(quotePrompt
|
||
? [
|
||
{
|
||
obj: ChatRoleEnum.System,
|
||
value: quotePrompt
|
||
}
|
||
]
|
||
: []),
|
||
...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({
|
||
messages: filterMessages
|
||
});
|
||
maxToken = maxToken + promptsToken > tokensLimit ? tokensLimit - promptsToken : maxToken;
|
||
|
||
return {
|
||
max_tokens: maxToken
|
||
};
|
||
}
|
||
|
||
function targetResponse({
|
||
res,
|
||
outputs,
|
||
detail
|
||
}: {
|
||
res: NextApiResponse;
|
||
outputs: AppModuleItemType['outputs'];
|
||
detail: boolean;
|
||
}) {
|
||
const targets =
|
||
outputs.find((output) => output.key === TaskResponseKeyEnum.answerText)?.targets || [];
|
||
|
||
if (targets.length === 0) return;
|
||
sseResponse({
|
||
res,
|
||
event: detail ? sseResponseEventEnum.answer : undefined,
|
||
data: textAdaptGptResponse({
|
||
text: '\n'
|
||
})
|
||
});
|
||
}
|
||
|
||
async function streamResponse({
|
||
res,
|
||
detail,
|
||
response
|
||
}: {
|
||
res: NextApiResponse;
|
||
detail: boolean;
|
||
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: detail ? sseResponseEventEnum.answer : undefined,
|
||
data: textAdaptGptResponse({
|
||
text: content
|
||
})
|
||
});
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.log('pipe error', error);
|
||
}
|
||
|
||
if (error) {
|
||
return Promise.reject(error);
|
||
}
|
||
|
||
return {
|
||
answer
|
||
};
|
||
}
|