perf: completion dispatch

This commit is contained in:
archer
2023-07-23 14:07:59 +08:00
parent 8151350d9f
commit 6027a966d2
33 changed files with 1797 additions and 2181 deletions

View File

@@ -0,0 +1,103 @@
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
import { ChatContextFilter } from '@/service/utils/chat/index';
import type { ChatHistoryItemResType, ChatItemType } from '@/types/chat';
import { ChatRoleEnum, TaskResponseKeyEnum } from '@/constants/chat';
import { getOpenAIApi, axiosConfig } from '@/service/ai/openai';
import type { ClassifyQuestionAgentItemType } from '@/types/app';
import { countModelPrice } from '@/service/events/pushBill';
export type CQProps = {
systemPrompt?: string;
history?: ChatItemType[];
userChatInput: string;
agents: ClassifyQuestionAgentItemType[];
};
export type CQResponse = {
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
[key: string]: any;
};
const moduleName = 'Classify Question';
const agentModel = 'gpt-3.5-turbo';
const agentFunName = 'agent_user_question';
const maxTokens = 2000;
/* request openai chat */
export const dispatchClassifyQuestion = async (props: Record<string, any>): Promise<CQResponse> => {
const { agents, systemPrompt, history = [], userChatInput } = props as CQProps;
const messages: ChatItemType[] = [
...(systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: systemPrompt
}
]
: []),
...history,
{
obj: ChatRoleEnum.Human,
value: userChatInput
}
];
const filterMessages = ChatContextFilter({
model: agentModel,
prompts: messages,
maxTokens
});
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.value},返回:'${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('');
}
const tokens = response.data.usage?.total_tokens || 0;
const result = agents.find((item) => item.key === arg.type) || agents[0];
return {
[result.key]: 1,
[TaskResponseKeyEnum.responseData]: {
moduleName,
price: countModelPrice({ model: agentModel, tokens }),
model: agentModel,
tokens,
cqList: agents,
cqResult: result.value
}
};
};

View File

@@ -0,0 +1,100 @@
// 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';
import { authUser } from '@/service/utils/auth';
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 {
await authUser({ req, authRoot: true });
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.value
};
});
// 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;
}

View File

@@ -0,0 +1,255 @@
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 { ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat';
import { parseStreamChunk, 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';
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;
};
const moduleName = 'AI Chat';
/* 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.');
}
// FastGpt temperature range: 1~10
temperature = +(modelConstantsData.maxTemperature * (temperature / 10)).toFixed(2);
const limitText = (() => {
if (limitPrompt) return limitPrompt;
if (quoteQA.length > 0 && !limitPrompt) {
return '根据知识库内容回答问题,仅回复知识库提供的内容,不要对知识库内容做补充说明。';
}
return '';
})();
const quotePrompt =
quoteQA.length > 0
? `下面是知识库内容:
${quoteQA.map((item, i) => `${i + 1}. [${item.q}\n${item.a}]`).join('\n')}
`
: '';
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 modelTokenLimit = getChatModel(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.countTokens({
model,
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 { answerText, totalTokens, finishMessages } = 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 = countOpenAIToken({
messages: finishMessages
});
return {
answerText: answer,
totalTokens,
finishMessages
};
} else {
const answer = stream ? '' : response.data.choices?.[0].message?.content || '';
const totalTokens = stream ? 0 : response.data.usage?.total_tokens || 0;
const finishMessages = filterMessages.concat({
obj: ChatRoleEnum.AI,
value: answer
});
return {
answerText: answer,
totalTokens,
finishMessages
};
}
})();
return {
[TaskResponseKeyEnum.answerText]: answerText,
[TaskResponseKeyEnum.responseData]: {
moduleName,
price: countModelPrice({ model, tokens: totalTokens }),
model: modelConstantsData.name,
tokens: totalTokens,
question: userChatInput,
answer: answerText,
maxToken,
finishMessages
}
};
};
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
};
}

View File

@@ -0,0 +1,6 @@
export * from './init/history';
export * from './init/userChatInput';
export * from './chat/oneapi';
export * from './kb/search';
export * from './tools/answer';
export * from './agent/classifyQuestion';

View File

@@ -0,0 +1,15 @@
import { SystemInputEnum } from '@/constants/app';
import { ChatItemType } from '@/types/chat';
export type HistoryProps = {
maxContext: number;
[SystemInputEnum.history]: ChatItemType[];
};
export const dispatchHistory = (props: Record<string, any>) => {
const { maxContext = 5, history = [] } = props as HistoryProps;
return {
history: history.slice(-maxContext)
};
};

View File

@@ -0,0 +1,12 @@
import { SystemInputEnum } from '@/constants/app';
export type UserChatInputProps = {
[SystemInputEnum.userChatInput]: string;
};
export const dispatchChatInput = (props: Record<string, any>) => {
const { userChatInput } = props as UserChatInputProps;
return {
userChatInput
};
};

View File

@@ -0,0 +1,76 @@
import { PgClient } from '@/service/pg';
import type { ChatHistoryItemResType, ChatItemType } from '@/types/chat';
import { TaskResponseKeyEnum } from '@/constants/chat';
import { getVector } from '@/pages/api/openapi/plugin/vector';
import { countModelPrice } from '@/service/events/pushBill';
import type { SelectedKbType } from '@/types/plugin';
import type { QuoteItemType } from '@/types/chat';
type KBSearchProps = {
kbList: SelectedKbType;
history: ChatItemType[];
similarity: number;
limit: number;
userChatInput: string;
};
export type KBSearchResponse = {
[TaskResponseKeyEnum.responseData]: ChatHistoryItemResType;
isEmpty?: boolean;
unEmpty?: boolean;
quoteQA: QuoteItemType[];
};
const moduleName = 'KB Search';
export async function dispatchKBSearch(props: Record<string, any>): Promise<KBSearchResponse> {
const {
kbList = [],
history = [],
similarity = 0.8,
limit = 5,
userChatInput
} = props as KBSearchProps;
if (kbList.length === 0) {
return Promise.reject("You didn't choose the knowledge base");
}
if (!userChatInput) {
return Promise.reject('Your input is empty');
}
// get vector
const vectorModel = global.vectorModels[0];
const { vectors, tokenLen } = await getVector({
model: vectorModel.model,
input: [userChatInput]
});
// search kb
const res: any = await PgClient.query(
`BEGIN;
SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10};
select kb_id,id,q,a,source from modelData where kb_id IN (${kbList
.map((item) => `'${item.kbId}'`)
.join(',')}) AND vector <#> '[${vectors[0]}]' < -${similarity} order by vector <#> '[${
vectors[0]
}]' limit ${limit};
COMMIT;`
);
const searchRes: QuoteItemType[] = res?.[2]?.rows || [];
return {
isEmpty: searchRes.length === 0 ? true : undefined,
unEmpty: searchRes.length > 0 ? true : undefined,
quoteQA: searchRes,
responseData: {
moduleName,
price: countModelPrice({ model: vectorModel.model, tokens: tokenLen }),
model: vectorModel.name,
tokens: tokenLen,
similarity,
limit
}
};
}

View File

@@ -0,0 +1,31 @@
import { sseResponseEventEnum, TaskResponseKeyEnum } from '@/constants/chat';
import { sseResponse } from '@/service/utils/tools';
import { textAdaptGptResponse } from '@/utils/adapt';
import type { NextApiResponse } from 'next';
export type AnswerProps = {
res: NextApiResponse;
text: string;
stream: boolean;
};
export type AnswerResponse = {
[TaskResponseKeyEnum.answerText]: string;
};
export const dispatchAnswer = (props: Record<string, any>): AnswerResponse => {
const { res, text = '', stream } = props as AnswerProps;
if (stream) {
sseResponse({
res,
event: sseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: text.replace(/\\n/g, '\n')
})
});
}
return {
[TaskResponseKeyEnum.answerText]: text
};
};