mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-30 10:28:42 +00:00
perf: completion dispatch
This commit is contained in:
@@ -1,114 +0,0 @@
|
||||
import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
interface ConfigType {
|
||||
headers?: { [key: string]: string };
|
||||
hold?: boolean;
|
||||
timeout?: number;
|
||||
}
|
||||
interface ResponseDataType {
|
||||
code: number;
|
||||
message: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求开始
|
||||
*/
|
||||
function requestStart(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {
|
||||
if (config.headers) {
|
||||
// config.headers.Authorization = getToken();
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求成功,检查请求头
|
||||
*/
|
||||
function responseSuccess(response: AxiosResponse<ResponseDataType>) {
|
||||
return response;
|
||||
}
|
||||
/**
|
||||
* 响应数据检查
|
||||
*/
|
||||
function checkRes(data: ResponseDataType) {
|
||||
if (data === undefined) {
|
||||
console.log('error->', data, 'data is empty');
|
||||
return Promise.reject('服务器异常');
|
||||
} else if (data.code < 200 || data.code >= 400) {
|
||||
return Promise.reject(data);
|
||||
}
|
||||
return data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应错误
|
||||
*/
|
||||
function responseError(err: any) {
|
||||
console.log('error->', '请求错误', err);
|
||||
|
||||
if (!err) {
|
||||
return Promise.reject({ message: '未知错误' });
|
||||
}
|
||||
if (typeof err === 'string') {
|
||||
return Promise.reject({ message: err });
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
/* 创建请求实例 */
|
||||
const instance = axios.create({
|
||||
timeout: 60000, // 超时时间
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
/* 请求拦截 */
|
||||
instance.interceptors.request.use(requestStart, (err) => Promise.reject(err));
|
||||
/* 响应拦截 */
|
||||
instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err));
|
||||
|
||||
function request(url: string, data: any, config: ConfigType, method: Method): any {
|
||||
/* 去空 */
|
||||
for (const key in data) {
|
||||
if (data[key] === null || data[key] === undefined) {
|
||||
delete data[key];
|
||||
}
|
||||
}
|
||||
|
||||
return instance
|
||||
.request({
|
||||
baseURL: `http://localhost:${process.env.PORT || 3000}/api`,
|
||||
url,
|
||||
method,
|
||||
data: ['POST', 'PUT'].includes(method) ? data : null,
|
||||
params: !['POST', 'PUT'].includes(method) ? data : null,
|
||||
...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>(url: string, params = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, params, config, 'GET');
|
||||
}
|
||||
|
||||
export function POST<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, data, config, 'POST');
|
||||
}
|
||||
|
||||
export function PUT<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, data, config, 'PUT');
|
||||
}
|
||||
|
||||
export function DELETE<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
|
||||
return request(url, data, config, 'DELETE');
|
||||
}
|
@@ -1,115 +0,0 @@
|
||||
import { sseResponseEventEnum } from '@/constants/chat';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { parseStreamChunk } from '@/utils/adapt';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { sseResponse } from '../utils/tools';
|
||||
import { TaskResponseKeyEnum } from '@/constants/app';
|
||||
|
||||
interface Props {
|
||||
res: NextApiResponse; // 用于流转发
|
||||
url: string;
|
||||
data: Record<string, any>;
|
||||
}
|
||||
export const moduleFetch = ({ url, data, res }: Props) =>
|
||||
new Promise<Record<string, any>>(async (resolve, reject) => {
|
||||
try {
|
||||
const abortSignal = new AbortController();
|
||||
const baseUrl = `http://localhost:${process.env.PORT || 3000}/api`;
|
||||
const requestUrl = url.startsWith('/') ? `${baseUrl}${url}` : url;
|
||||
const response = await fetch(requestUrl, {
|
||||
method: 'POST',
|
||||
// @ts-ignore
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
rootkey: process.env.ROOT_KEY
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
signal: abortSignal.signal
|
||||
});
|
||||
|
||||
if (response.status >= 300 || response.status < 200) {
|
||||
const err = await response.json();
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
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();
|
||||
return resolve(jsonResponse?.data || {});
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
let chatResponse: Record<string, any> = {
|
||||
[TaskResponseKeyEnum.answerText]: ''
|
||||
};
|
||||
|
||||
const read = async () => {
|
||||
try {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
return resolve(chatResponse);
|
||||
} else if (res.closed) {
|
||||
resolve(chatResponse);
|
||||
abortSignal.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
const chunkResponse = parseStreamChunk(value);
|
||||
|
||||
chunkResponse.forEach((item) => {
|
||||
// parse json data
|
||||
const data = (() => {
|
||||
try {
|
||||
return JSON.parse(item.data);
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
if (!res.closed && item.event === sseResponseEventEnum.moduleFetchResponse) {
|
||||
chatResponse = {
|
||||
...chatResponse,
|
||||
...data
|
||||
};
|
||||
} else if (
|
||||
!res.closed &&
|
||||
item.event === sseResponseEventEnum.answer &&
|
||||
data?.choices?.[0]?.delta
|
||||
) {
|
||||
// save answer
|
||||
const answer: string = data?.choices?.[0].delta.content || '';
|
||||
if (answer) {
|
||||
chatResponse = {
|
||||
...chatResponse,
|
||||
[TaskResponseKeyEnum.answerText]:
|
||||
chatResponse[TaskResponseKeyEnum.answerText] + answer
|
||||
};
|
||||
}
|
||||
|
||||
sseResponse({
|
||||
res,
|
||||
event: sseResponseEventEnum.answer,
|
||||
data: JSON.stringify(data)
|
||||
});
|
||||
} else if (item.event === sseResponseEventEnum.error) {
|
||||
return reject(data);
|
||||
}
|
||||
});
|
||||
read();
|
||||
} catch (err: any) {
|
||||
if (err?.message === 'The operation was aborted.') {
|
||||
return;
|
||||
}
|
||||
reject(getErrText(err, '请求异常'));
|
||||
}
|
||||
};
|
||||
read();
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
reject(getErrText(err, '请求异常'));
|
||||
}
|
||||
});
|
@@ -1,93 +1,54 @@
|
||||
import { connectToDatabase, Bill, User, ShareChat } from '../mongo';
|
||||
import { BillSourceEnum } from '@/constants/user';
|
||||
import { getModel } from '../utils/data';
|
||||
import type { BillListItemType } from '@/types/mongoSchema';
|
||||
import { ChatHistoryItemResType } from '@/types/chat';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
export const createTaskBill = async ({
|
||||
export const pushTaskBill = async ({
|
||||
appName,
|
||||
appId,
|
||||
userId,
|
||||
source
|
||||
source,
|
||||
shareId,
|
||||
response
|
||||
}: {
|
||||
appName: string;
|
||||
appId: string;
|
||||
userId: string;
|
||||
source: `${BillSourceEnum}`;
|
||||
shareId?: string;
|
||||
response: ChatHistoryItemResType[];
|
||||
}) => {
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
appName,
|
||||
appId,
|
||||
total: 0,
|
||||
source,
|
||||
list: []
|
||||
});
|
||||
return String(res._id);
|
||||
};
|
||||
const total = response.reduce((sum, item) => sum + item.price, 0);
|
||||
|
||||
export const pushTaskBillListItem = async ({
|
||||
billId,
|
||||
moduleName,
|
||||
amount,
|
||||
model,
|
||||
tokenLen
|
||||
}: { billId?: string } & BillListItemType) => {
|
||||
if (!billId) return;
|
||||
try {
|
||||
await Bill.findByIdAndUpdate(billId, {
|
||||
$push: {
|
||||
list: {
|
||||
moduleName,
|
||||
amount,
|
||||
model,
|
||||
tokenLen
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
export const finishTaskBill = async ({ billId, shareId }: { billId: string; shareId?: string }) => {
|
||||
try {
|
||||
// update bill
|
||||
const res = await Bill.findByIdAndUpdate(billId, [
|
||||
{
|
||||
$set: {
|
||||
total: {
|
||||
$sum: '$list.amount'
|
||||
},
|
||||
time: new Date()
|
||||
}
|
||||
}
|
||||
]);
|
||||
if (!res) return;
|
||||
const total = res.list.reduce((sum, item) => sum + item.amount, 0) || 0;
|
||||
|
||||
if (shareId) {
|
||||
updateShareChatBill({
|
||||
shareId,
|
||||
total
|
||||
});
|
||||
}
|
||||
|
||||
console.log('finish bill:', formatPrice(total));
|
||||
|
||||
// 账号扣费
|
||||
await User.findByIdAndUpdate(res.userId, {
|
||||
await Promise.allSettled([
|
||||
Bill.create({
|
||||
userId,
|
||||
appName,
|
||||
appId,
|
||||
total,
|
||||
source,
|
||||
list: response.map((item) => ({
|
||||
moduleName: item.moduleName,
|
||||
amount: item.price || 0,
|
||||
model: item.model,
|
||||
tokenLen: item.tokens
|
||||
}))
|
||||
}),
|
||||
User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: -total }
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Finish bill failed:', error);
|
||||
billId && Bill.findByIdAndDelete(billId);
|
||||
}
|
||||
};
|
||||
}),
|
||||
...(shareId
|
||||
? [
|
||||
updateShareChatBill({
|
||||
shareId,
|
||||
total
|
||||
})
|
||||
]
|
||||
: [])
|
||||
]);
|
||||
|
||||
export const delTaskBill = async (billId?: string) => {
|
||||
if (!billId) return;
|
||||
|
||||
try {
|
||||
await Bill.findByIdAndRemove(billId);
|
||||
} catch (error) {}
|
||||
console.log('finish bill:', formatPrice(total));
|
||||
};
|
||||
|
||||
export const updateShareChatBill = async ({
|
||||
|
103
client/src/service/moduleDispatch/agent/classifyQuestion.ts
Normal file
103
client/src/service/moduleDispatch/agent/classifyQuestion.ts
Normal 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
|
||||
}
|
||||
};
|
||||
};
|
100
client/src/service/moduleDispatch/agent/extract.ts
Normal file
100
client/src/service/moduleDispatch/agent/extract.ts
Normal 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;
|
||||
}
|
255
client/src/service/moduleDispatch/chat/oneapi.ts
Normal file
255
client/src/service/moduleDispatch/chat/oneapi.ts
Normal 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
|
||||
};
|
||||
}
|
6
client/src/service/moduleDispatch/index.ts
Normal file
6
client/src/service/moduleDispatch/index.ts
Normal 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';
|
15
client/src/service/moduleDispatch/init/history.tsx
Normal file
15
client/src/service/moduleDispatch/init/history.tsx
Normal 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)
|
||||
};
|
||||
};
|
12
client/src/service/moduleDispatch/init/userChatInput.tsx
Normal file
12
client/src/service/moduleDispatch/init/userChatInput.tsx
Normal 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
|
||||
};
|
||||
};
|
76
client/src/service/moduleDispatch/kb/search.ts
Normal file
76
client/src/service/moduleDispatch/kb/search.ts
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
31
client/src/service/moduleDispatch/tools/answer.ts
Normal file
31
client/src/service/moduleDispatch/tools/answer.ts
Normal 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
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user