mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-24 13:53:50 +00:00
feat: chat quote
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import { GET, POST, DELETE } from './request';
|
import { GET, POST, DELETE } from './request';
|
||||||
import type { ChatItemType, HistoryItemType } from '@/types/chat';
|
import type { HistoryItemType } from '@/types/chat';
|
||||||
import type { InitChatResponse, InitShareChatResponse } from './response/chat';
|
import type { InitChatResponse, InitShareChatResponse } from './response/chat';
|
||||||
import { RequestPaging } from '../types/index';
|
import { RequestPaging } from '../types/index';
|
||||||
import type { ShareChatSchema } from '@/types/mongoSchema';
|
import type { ShareChatSchema } from '@/types/mongoSchema';
|
||||||
import type { ShareChatEditType } from '@/types/model';
|
import type { ShareChatEditType } from '@/types/model';
|
||||||
import { Obj2Query } from '@/utils/tools';
|
import { Obj2Query } from '@/utils/tools';
|
||||||
|
import { Response as LastChatResultResponseType } from '@/pages/api/openapi/chat/lastChatResult';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取初始化聊天内容
|
* 获取初始化聊天内容
|
||||||
@@ -24,15 +25,10 @@ export const getChatHistory = (data: RequestPaging) =>
|
|||||||
export const delChatHistoryById = (id: string) => GET(`/chat/removeHistory?id=${id}`);
|
export const delChatHistoryById = (id: string) => GET(`/chat/removeHistory?id=${id}`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 存储一轮对话
|
* get latest chat result by chatId
|
||||||
*/
|
*/
|
||||||
export const postSaveChat = (data: {
|
export const getChatResult = (chatId: string) =>
|
||||||
modelId: string;
|
GET<LastChatResultResponseType>('/openapi/chat/lastChatResult', { chatId });
|
||||||
newChatId: '' | string;
|
|
||||||
chatId: '' | string;
|
|
||||||
prompts: [ChatItemType, ChatItemType];
|
|
||||||
}) => POST<string>('/chat/saveChat', data);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除一句对话
|
* 删除一句对话
|
||||||
*/
|
*/
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { SYSTEM_PROMPT_HEADER, NEW_CHATID_HEADER } from '@/constants/chat';
|
import { NEW_CHATID_HEADER } from '@/constants/chat';
|
||||||
|
|
||||||
interface StreamFetchProps {
|
interface StreamFetchProps {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -7,55 +7,52 @@ interface StreamFetchProps {
|
|||||||
abortSignal: AbortController;
|
abortSignal: AbortController;
|
||||||
}
|
}
|
||||||
export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
|
export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
|
||||||
new Promise<{ responseText: string; systemPrompt: string; newChatId: string }>(
|
new Promise<{ responseText: string; newChatId: string }>(async (resolve, reject) => {
|
||||||
async (resolve, reject) => {
|
try {
|
||||||
try {
|
const res = await fetch(url, {
|
||||||
const res = await fetch(url, {
|
method: 'POST',
|
||||||
method: 'POST',
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json'
|
||||||
'Content-Type': 'application/json'
|
},
|
||||||
},
|
body: JSON.stringify(data),
|
||||||
body: JSON.stringify(data),
|
signal: abortSignal.signal
|
||||||
signal: abortSignal.signal
|
});
|
||||||
});
|
const reader = res.body?.getReader();
|
||||||
const reader = res.body?.getReader();
|
if (!reader) return;
|
||||||
if (!reader) return;
|
|
||||||
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
const systemPrompt = decodeURIComponent(res.headers.get(SYSTEM_PROMPT_HEADER) || '').trim();
|
const newChatId = decodeURIComponent(res.headers.get(NEW_CHATID_HEADER) || '');
|
||||||
const newChatId = decodeURIComponent(res.headers.get(NEW_CHATID_HEADER) || '');
|
|
||||||
|
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
|
|
||||||
const read = async () => {
|
const read = async () => {
|
||||||
try {
|
try {
|
||||||
const { done, value } = await reader?.read();
|
const { done, value } = await reader?.read();
|
||||||
if (done) {
|
if (done) {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
resolve({ responseText, systemPrompt, newChatId });
|
resolve({ responseText, newChatId });
|
||||||
} else {
|
} else {
|
||||||
const parseError = JSON.parse(responseText);
|
const parseError = JSON.parse(responseText);
|
||||||
reject(parseError?.message || '请求异常');
|
reject(parseError?.message || '请求异常');
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const text = decoder.decode(value);
|
|
||||||
responseText += text;
|
return;
|
||||||
onMessage(text);
|
|
||||||
read();
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err?.message === 'The user aborted a request.') {
|
|
||||||
return resolve({ responseText, systemPrompt, newChatId });
|
|
||||||
}
|
|
||||||
reject(typeof err === 'string' ? err : err?.message || '请求异常');
|
|
||||||
}
|
}
|
||||||
};
|
const text = decoder.decode(value);
|
||||||
read();
|
responseText += text;
|
||||||
} catch (err: any) {
|
onMessage(text);
|
||||||
console.log(err, '====');
|
read();
|
||||||
reject(typeof err === 'string' ? err : err?.message || '请求异常');
|
} catch (err: any) {
|
||||||
}
|
if (err?.message === 'The user aborted a request.') {
|
||||||
|
return resolve({ responseText, newChatId });
|
||||||
|
}
|
||||||
|
reject(typeof err === 'string' ? err : err?.message || '请求异常');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
read();
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log(err, '====');
|
||||||
|
reject(typeof err === 'string' ? err : err?.message || '请求异常');
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
export const SYSTEM_PROMPT_HEADER = 'System-Prompt-Header';
|
export const NEW_CHATID_HEADER = 'response-new-chat-id';
|
||||||
export const NEW_CHATID_HEADER = 'Chat-Id-Header';
|
|
||||||
|
|
||||||
export enum ChatRoleEnum {
|
export enum ChatRoleEnum {
|
||||||
System = 'System',
|
System = 'System',
|
||||||
|
@@ -55,7 +55,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||||||
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
|
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
|
||||||
<Script src="/js/pdf.js" strategy="lazyOnload"></Script>
|
<Script src="/js/pdf.js" strategy="lazyOnload"></Script>
|
||||||
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
|
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
|
||||||
<Script src="/js/particles.js" strategy="lazyOnload"></Script>
|
<Script src="/js/particles.js"></Script>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ChakraProvider theme={theme}>
|
<ChakraProvider theme={theme}>
|
||||||
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
||||||
|
@@ -2,19 +2,24 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { connectToDatabase } from '@/service/mongo';
|
import { connectToDatabase } from '@/service/mongo';
|
||||||
import { authChat } from '@/service/utils/auth';
|
import { authChat } from '@/service/utils/auth';
|
||||||
import { modelServiceToolMap } from '@/service/utils/chat';
|
import { modelServiceToolMap } from '@/service/utils/chat';
|
||||||
import { ChatItemSimpleType } from '@/types/chat';
|
import { ChatItemType } from '@/types/chat';
|
||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
|
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
|
||||||
import { pushChatBill } from '@/service/events/pushBill';
|
import { pushChatBill } from '@/service/events/pushBill';
|
||||||
import { resStreamResponse } from '@/service/utils/chat';
|
import { resStreamResponse } from '@/service/utils/chat';
|
||||||
import { searchKb } from '@/service/plugins/searchKb';
|
import { appKbSearch } from '../openapi/kb/appKbSearch';
|
||||||
import { ChatRoleEnum } from '@/constants/chat';
|
import { ChatRoleEnum } from '@/constants/chat';
|
||||||
import { BillTypeEnum } from '@/constants/user';
|
import { BillTypeEnum } from '@/constants/user';
|
||||||
import { sensitiveCheck } from '@/service/api/text';
|
import { sensitiveCheck } from '@/service/api/text';
|
||||||
|
import { NEW_CHATID_HEADER } from '@/constants/chat';
|
||||||
|
import { saveChat } from './saveChat';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
|
||||||
/* 发送提示词 */
|
/* 发送提示词 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
let step = 0; // step=1时,表示开始了流响应
|
res.on('close', () => {
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
res.on('error', () => {
|
res.on('error', () => {
|
||||||
console.log('error: ', 'request error');
|
console.log('error: ', 'request error');
|
||||||
res.end();
|
res.end();
|
||||||
@@ -22,9 +27,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { chatId, prompt, modelId } = req.body as {
|
const { chatId, prompt, modelId } = req.body as {
|
||||||
prompt: ChatItemSimpleType;
|
prompt: [ChatItemType, ChatItemType];
|
||||||
modelId: string;
|
modelId: string;
|
||||||
chatId: '' | string;
|
chatId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!modelId || !prompt) {
|
if (!modelId || !prompt) {
|
||||||
@@ -44,42 +49,69 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
||||||
|
|
||||||
// 读取对话内容
|
// 读取对话内容
|
||||||
const prompts = [...content, prompt];
|
const prompts = [...content, prompt[0]];
|
||||||
let systemPrompts: {
|
const {
|
||||||
obj: ChatRoleEnum;
|
code = 200,
|
||||||
value: string;
|
systemPrompts = [],
|
||||||
}[] = [];
|
quote = []
|
||||||
|
} = await (async () => {
|
||||||
|
// 使用了知识库搜索
|
||||||
|
if (model.chat.relatedKbs.length > 0) {
|
||||||
|
const { code, searchPrompts, rawSearch } = await appKbSearch({
|
||||||
|
model,
|
||||||
|
userId,
|
||||||
|
prompts,
|
||||||
|
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity
|
||||||
|
});
|
||||||
|
|
||||||
// 使用了知识库搜索
|
return {
|
||||||
if (model.chat.relatedKbs.length > 0) {
|
code,
|
||||||
const { code, searchPrompts } = await searchKb({
|
quote: rawSearch,
|
||||||
userOpenAiKey,
|
systemPrompts: searchPrompts
|
||||||
prompts,
|
};
|
||||||
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
|
}
|
||||||
model,
|
if (model.chat.systemPrompt) {
|
||||||
|
return {
|
||||||
|
systemPrompts: [
|
||||||
|
{
|
||||||
|
obj: ChatRoleEnum.System,
|
||||||
|
value: model.chat.systemPrompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// get conversationId. create a newId if it is null
|
||||||
|
const conversationId = chatId || String(new Types.ObjectId());
|
||||||
|
!chatId && res?.setHeader(NEW_CHATID_HEADER, conversationId);
|
||||||
|
|
||||||
|
// search result is empty
|
||||||
|
if (code === 201) {
|
||||||
|
const response = systemPrompts[0]?.value;
|
||||||
|
await saveChat({
|
||||||
|
chatId,
|
||||||
|
newChatId: conversationId,
|
||||||
|
modelId,
|
||||||
|
prompts: [
|
||||||
|
prompt[0],
|
||||||
|
{
|
||||||
|
...prompt[1],
|
||||||
|
quote: [],
|
||||||
|
value: response
|
||||||
|
}
|
||||||
|
],
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
return res.end(response);
|
||||||
// search result is empty
|
|
||||||
if (code === 201) {
|
|
||||||
return res.send(searchPrompts[0]?.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
systemPrompts = searchPrompts;
|
|
||||||
} else if (model.chat.systemPrompt) {
|
|
||||||
systemPrompts = [
|
|
||||||
{
|
|
||||||
obj: ChatRoleEnum.System,
|
|
||||||
value: model.chat.systemPrompt
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.splice(prompts.length - 3, 0, ...systemPrompts);
|
prompts.splice(prompts.length - 3, 0, ...systemPrompts);
|
||||||
|
|
||||||
// content check
|
// content check
|
||||||
await sensitiveCheck({
|
await sensitiveCheck({
|
||||||
input: [...systemPrompts, prompt].map((item) => item.value).join('')
|
input: [...systemPrompts, prompt[0]].map((item) => item.value).join('')
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算温度
|
// 计算温度
|
||||||
@@ -87,54 +119,65 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
|
||||||
// 发出请求
|
// 发出 chat 请求
|
||||||
const { streamResponse } = await modelServiceToolMap[model.chat.chatModel].chatCompletion({
|
const { streamResponse } = await modelServiceToolMap[model.chat.chatModel].chatCompletion({
|
||||||
apiKey: userOpenAiKey || systemAuthKey,
|
apiKey: userOpenAiKey || systemAuthKey,
|
||||||
temperature: +temperature,
|
temperature: +temperature,
|
||||||
messages: prompts,
|
messages: prompts,
|
||||||
stream: true,
|
stream: true,
|
||||||
res,
|
res,
|
||||||
chatId
|
chatId: conversationId
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||||
|
|
||||||
step = 1;
|
if (res.closed) return res.end();
|
||||||
|
|
||||||
const { totalTokens, finishMessages } = await resStreamResponse({
|
try {
|
||||||
model: model.chat.chatModel,
|
const { totalTokens, finishMessages, responseContent } = await resStreamResponse({
|
||||||
res,
|
model: model.chat.chatModel,
|
||||||
chatResponse: streamResponse,
|
res,
|
||||||
prompts,
|
chatResponse: streamResponse,
|
||||||
systemPrompt: showModelDetail
|
prompts
|
||||||
? prompts
|
|
||||||
.filter((item) => item.obj === ChatRoleEnum.System)
|
|
||||||
.map((item) => item.value)
|
|
||||||
.join('\n')
|
|
||||||
: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// 只有使用平台的 key 才计费
|
|
||||||
pushChatBill({
|
|
||||||
isPay: !userOpenAiKey,
|
|
||||||
chatModel: model.chat.chatModel,
|
|
||||||
userId,
|
|
||||||
chatId,
|
|
||||||
textLen: finishMessages.map((item) => item.value).join('').length,
|
|
||||||
tokens: totalTokens,
|
|
||||||
type: BillTypeEnum.chat
|
|
||||||
});
|
|
||||||
} catch (err: any) {
|
|
||||||
if (step === 1) {
|
|
||||||
// 直接结束流
|
|
||||||
res.end();
|
|
||||||
console.log('error,结束');
|
|
||||||
} else {
|
|
||||||
res.status(500);
|
|
||||||
jsonRes(res, {
|
|
||||||
code: 500,
|
|
||||||
error: err
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// save chat
|
||||||
|
await saveChat({
|
||||||
|
chatId,
|
||||||
|
newChatId: conversationId,
|
||||||
|
modelId,
|
||||||
|
prompts: [
|
||||||
|
prompt[0],
|
||||||
|
{
|
||||||
|
...prompt[1],
|
||||||
|
quote: showModelDetail ? quote : [],
|
||||||
|
value: responseContent
|
||||||
|
}
|
||||||
|
],
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
// 只有使用平台的 key 才计费
|
||||||
|
pushChatBill({
|
||||||
|
isPay: !userOpenAiKey,
|
||||||
|
chatModel: model.chat.chatModel,
|
||||||
|
userId,
|
||||||
|
chatId: conversationId,
|
||||||
|
textLen: finishMessages.map((item) => item.value).join('').length,
|
||||||
|
tokens: totalTokens,
|
||||||
|
type: BillTypeEnum.chat
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.end();
|
||||||
|
console.log('error,结束', error);
|
||||||
}
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(500);
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -73,7 +73,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
_id: '$content._id',
|
_id: '$content._id',
|
||||||
obj: '$content.obj',
|
obj: '$content.obj',
|
||||||
value: '$content.value',
|
value: '$content.value',
|
||||||
systemPrompt: '$content.systemPrompt'
|
quote: '$content.quote'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
@@ -6,15 +6,17 @@ import { authModel } from '@/service/utils/auth';
|
|||||||
import { authUser } from '@/service/utils/auth';
|
import { authUser } from '@/service/utils/auth';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
newChatId?: string;
|
||||||
|
chatId?: string;
|
||||||
|
modelId: string;
|
||||||
|
prompts: [ChatItemType, ChatItemType];
|
||||||
|
};
|
||||||
|
|
||||||
/* 聊天内容存存储 */
|
/* 聊天内容存存储 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const { chatId, modelId, prompts, newChatId } = req.body as {
|
const { chatId, modelId, prompts, newChatId } = req.body as Props;
|
||||||
newChatId: '' | string;
|
|
||||||
chatId: '' | string;
|
|
||||||
modelId: string;
|
|
||||||
prompts: [ChatItemType, ChatItemType];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!prompts) {
|
if (!prompts) {
|
||||||
throw new Error('缺少参数');
|
throw new Error('缺少参数');
|
||||||
@@ -22,44 +24,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
const { userId } = await authUser({ req, authToken: true });
|
const { userId } = await authUser({ req, authToken: true });
|
||||||
|
|
||||||
await connectToDatabase();
|
const nId = await saveChat({
|
||||||
|
chatId,
|
||||||
|
modelId,
|
||||||
|
prompts,
|
||||||
|
newChatId,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
const content = prompts.map((item) => ({
|
jsonRes(res, {
|
||||||
_id: new mongoose.Types.ObjectId(item._id),
|
data: nId
|
||||||
obj: item.obj,
|
});
|
||||||
value: item.value,
|
|
||||||
systemPrompt: item.systemPrompt
|
|
||||||
}));
|
|
||||||
|
|
||||||
await authModel({ modelId, userId, authOwner: false });
|
|
||||||
|
|
||||||
// 没有 chatId, 创建一个对话
|
|
||||||
if (!chatId) {
|
|
||||||
const { _id } = await Chat.create({
|
|
||||||
_id: newChatId ? new mongoose.Types.ObjectId(newChatId) : undefined,
|
|
||||||
userId,
|
|
||||||
modelId,
|
|
||||||
content,
|
|
||||||
title: content[0].value.slice(0, 20),
|
|
||||||
latestChat: content[1].value
|
|
||||||
});
|
|
||||||
return jsonRes(res, {
|
|
||||||
data: _id
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 已经有记录,追加入库
|
|
||||||
await Chat.findByIdAndUpdate(chatId, {
|
|
||||||
$push: {
|
|
||||||
content: {
|
|
||||||
$each: content
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title: content[0].value.slice(0, 20),
|
|
||||||
latestChat: content[1].value,
|
|
||||||
updateTime: new Date()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
jsonRes(res);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
code: 500,
|
code: 500,
|
||||||
@@ -67,3 +42,46 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveChat({
|
||||||
|
chatId,
|
||||||
|
newChatId,
|
||||||
|
modelId,
|
||||||
|
prompts,
|
||||||
|
userId
|
||||||
|
}: Props & { userId: string }) {
|
||||||
|
await connectToDatabase();
|
||||||
|
await authModel({ modelId, userId, authOwner: false });
|
||||||
|
|
||||||
|
const content = prompts.map((item) => ({
|
||||||
|
_id: item._id ? new mongoose.Types.ObjectId(item._id) : undefined,
|
||||||
|
obj: item.obj,
|
||||||
|
value: item.value,
|
||||||
|
quote: item.quote
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 没有 chatId, 创建一个对话
|
||||||
|
if (!chatId) {
|
||||||
|
const { _id } = await Chat.create({
|
||||||
|
_id: newChatId ? new mongoose.Types.ObjectId(newChatId) : undefined,
|
||||||
|
userId,
|
||||||
|
modelId,
|
||||||
|
content,
|
||||||
|
title: content[0].value.slice(0, 20),
|
||||||
|
latestChat: content[1].value
|
||||||
|
});
|
||||||
|
return _id;
|
||||||
|
} else {
|
||||||
|
// 已经有记录,追加入库
|
||||||
|
await Chat.findByIdAndUpdate(chatId, {
|
||||||
|
$push: {
|
||||||
|
content: {
|
||||||
|
$each: content
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: content[0].value.slice(0, 20),
|
||||||
|
latestChat: content[1].value,
|
||||||
|
updateTime: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -7,14 +7,13 @@ import { jsonRes } from '@/service/response';
|
|||||||
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
|
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
|
||||||
import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
|
import { pushChatBill, updateShareChatBill } from '@/service/events/pushBill';
|
||||||
import { resStreamResponse } from '@/service/utils/chat';
|
import { resStreamResponse } from '@/service/utils/chat';
|
||||||
import { searchKb } from '@/service/plugins/searchKb';
|
|
||||||
import { ChatRoleEnum } from '@/constants/chat';
|
import { ChatRoleEnum } from '@/constants/chat';
|
||||||
import { BillTypeEnum } from '@/constants/user';
|
import { BillTypeEnum } from '@/constants/user';
|
||||||
import { sensitiveCheck } from '@/service/api/text';
|
import { sensitiveCheck } from '@/service/api/text';
|
||||||
|
import { appKbSearch } from '../../openapi/kb/appKbSearch';
|
||||||
|
|
||||||
/* 发送提示词 */
|
/* 发送提示词 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
let step = 0; // step=1 时,表示开始了流响应
|
|
||||||
res.on('error', () => {
|
res.on('error', () => {
|
||||||
console.log('error: ', 'request error');
|
console.log('error: ', 'request error');
|
||||||
res.end();
|
res.end();
|
||||||
@@ -42,34 +41,37 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
||||||
|
|
||||||
let systemPrompts: {
|
const { code = 200, systemPrompts = [] } = await (async () => {
|
||||||
obj: ChatRoleEnum;
|
// 使用了知识库搜索
|
||||||
value: string;
|
if (model.chat.relatedKbs.length > 0) {
|
||||||
}[] = [];
|
const { code, searchPrompts } = await appKbSearch({
|
||||||
|
model,
|
||||||
|
userId,
|
||||||
|
prompts,
|
||||||
|
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity
|
||||||
|
});
|
||||||
|
|
||||||
// 使用了知识库搜索
|
return {
|
||||||
if (model.chat.relatedKbs.length > 0) {
|
code,
|
||||||
const { code, searchPrompts } = await searchKb({
|
systemPrompts: searchPrompts
|
||||||
userOpenAiKey,
|
};
|
||||||
prompts,
|
|
||||||
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
|
|
||||||
model,
|
|
||||||
userId
|
|
||||||
});
|
|
||||||
|
|
||||||
// search result is empty
|
|
||||||
if (code === 201) {
|
|
||||||
return res.send(searchPrompts[0]?.value);
|
|
||||||
}
|
}
|
||||||
|
if (model.chat.systemPrompt) {
|
||||||
|
return {
|
||||||
|
systemPrompts: [
|
||||||
|
{
|
||||||
|
obj: ChatRoleEnum.System,
|
||||||
|
value: model.chat.systemPrompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
})();
|
||||||
|
|
||||||
systemPrompts = searchPrompts;
|
// search result is empty
|
||||||
} else if (model.chat.systemPrompt) {
|
if (code === 201) {
|
||||||
systemPrompts = [
|
return res.send(systemPrompts[0]?.value);
|
||||||
{
|
|
||||||
obj: ChatRoleEnum.System,
|
|
||||||
value: model.chat.systemPrompt
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.splice(prompts.length - 3, 0, ...systemPrompts);
|
prompts.splice(prompts.length - 3, 0, ...systemPrompts);
|
||||||
@@ -96,40 +98,40 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||||
|
|
||||||
step = 1;
|
if (res.closed) return res.end();
|
||||||
|
|
||||||
const { totalTokens, finishMessages } = await resStreamResponse({
|
try {
|
||||||
model: model.chat.chatModel,
|
const { totalTokens, finishMessages } = await resStreamResponse({
|
||||||
res,
|
model: model.chat.chatModel,
|
||||||
chatResponse: streamResponse,
|
res,
|
||||||
prompts,
|
chatResponse: streamResponse,
|
||||||
systemPrompt: ''
|
prompts
|
||||||
});
|
|
||||||
|
|
||||||
/* bill */
|
|
||||||
pushChatBill({
|
|
||||||
isPay: !userOpenAiKey,
|
|
||||||
chatModel: model.chat.chatModel,
|
|
||||||
userId,
|
|
||||||
textLen: finishMessages.map((item) => item.value).join('').length,
|
|
||||||
tokens: totalTokens,
|
|
||||||
type: BillTypeEnum.chat
|
|
||||||
});
|
|
||||||
updateShareChatBill({
|
|
||||||
shareId,
|
|
||||||
tokens: totalTokens
|
|
||||||
});
|
|
||||||
} catch (err: any) {
|
|
||||||
if (step === 1) {
|
|
||||||
// 直接结束流
|
|
||||||
res.end();
|
|
||||||
console.log('error,结束');
|
|
||||||
} else {
|
|
||||||
res.status(500);
|
|
||||||
jsonRes(res, {
|
|
||||||
code: 500,
|
|
||||||
error: err
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
|
||||||
|
/* bill */
|
||||||
|
pushChatBill({
|
||||||
|
isPay: !userOpenAiKey,
|
||||||
|
chatModel: model.chat.chatModel,
|
||||||
|
userId,
|
||||||
|
textLen: finishMessages.map((item) => item.value).join('').length,
|
||||||
|
tokens: totalTokens,
|
||||||
|
type: BillTypeEnum.chat
|
||||||
|
});
|
||||||
|
updateShareChatBill({
|
||||||
|
shareId,
|
||||||
|
tokens: totalTokens
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.end();
|
||||||
|
console.log('error,结束', error);
|
||||||
}
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(500);
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,15 +6,19 @@ import { ChatItemSimpleType } from '@/types/chat';
|
|||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
|
import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
|
||||||
import { pushChatBill } from '@/service/events/pushBill';
|
import { pushChatBill } from '@/service/events/pushBill';
|
||||||
import { searchKb } from '@/service/plugins/searchKb';
|
|
||||||
import { ChatRoleEnum } from '@/constants/chat';
|
import { ChatRoleEnum } from '@/constants/chat';
|
||||||
import { withNextCors } from '@/service/utils/tools';
|
import { withNextCors } from '@/service/utils/tools';
|
||||||
import { BillTypeEnum } from '@/constants/user';
|
import { BillTypeEnum } from '@/constants/user';
|
||||||
import { sensitiveCheck } from '@/service/api/text';
|
import { sensitiveCheck } from '@/service/api/text';
|
||||||
|
import { NEW_CHATID_HEADER } from '@/constants/chat';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { appKbSearch } from '../kb/appKbSearch';
|
||||||
|
|
||||||
/* 发送提示词 */
|
/* 发送提示词 */
|
||||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
let step = 0; // step=1时,表示开始了流响应
|
res.on('close', () => {
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
res.on('error', () => {
|
res.on('error', () => {
|
||||||
console.log('error: ', 'request error');
|
console.log('error: ', 'request error');
|
||||||
res.end();
|
res.end();
|
||||||
@@ -70,7 +74,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
|
|
||||||
// 使用了知识库搜索
|
// 使用了知识库搜索
|
||||||
if (model.chat.relatedKbs.length > 0) {
|
if (model.chat.relatedKbs.length > 0) {
|
||||||
const { code, searchPrompts } = await searchKb({
|
const { code, searchPrompts } = await appKbSearch({
|
||||||
prompts,
|
prompts,
|
||||||
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
|
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
|
||||||
model,
|
model,
|
||||||
@@ -109,6 +113,10 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// get conversationId. create a newId if it is null
|
||||||
|
const conversationId = chatId || String(new Types.ObjectId());
|
||||||
|
!chatId && res?.setHeader(NEW_CHATID_HEADER, conversationId);
|
||||||
|
|
||||||
// 发出请求
|
// 发出请求
|
||||||
const { streamResponse, responseMessages, responseText, totalTokens } =
|
const { streamResponse, responseMessages, responseText, totalTokens } =
|
||||||
await modelServiceToolMap[model.chat.chatModel].chatCompletion({
|
await modelServiceToolMap[model.chat.chatModel].chatCompletion({
|
||||||
@@ -117,30 +125,41 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
messages: prompts,
|
messages: prompts,
|
||||||
stream: isStream,
|
stream: isStream,
|
||||||
res,
|
res,
|
||||||
chatId
|
chatId: conversationId
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||||
|
|
||||||
let textLen = 0;
|
if (res.closed) return res.end();
|
||||||
let tokens = totalTokens;
|
|
||||||
|
|
||||||
if (isStream) {
|
const { textLen = 0, tokens = totalTokens } = await (async () => {
|
||||||
step = 1;
|
if (isStream) {
|
||||||
const { finishMessages, totalTokens } = await resStreamResponse({
|
try {
|
||||||
model: model.chat.chatModel,
|
const { finishMessages, totalTokens } = await resStreamResponse({
|
||||||
res,
|
model: model.chat.chatModel,
|
||||||
chatResponse: streamResponse,
|
res,
|
||||||
prompts
|
chatResponse: streamResponse,
|
||||||
});
|
prompts
|
||||||
textLen = finishMessages.map((item) => item.value).join('').length;
|
});
|
||||||
tokens = totalTokens;
|
res.end();
|
||||||
} else {
|
return {
|
||||||
textLen = responseMessages.map((item) => item.value).join('').length;
|
textLen: finishMessages.map((item) => item.value).join('').length,
|
||||||
jsonRes(res, {
|
tokens: totalTokens
|
||||||
data: responseText
|
};
|
||||||
});
|
} catch (error) {
|
||||||
}
|
res.end();
|
||||||
|
console.log('error,结束', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jsonRes(res, {
|
||||||
|
data: responseText
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
textLen: responseMessages.map((item) => item.value).join('').length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
})();
|
||||||
|
|
||||||
pushChatBill({
|
pushChatBill({
|
||||||
isPay: true,
|
isPay: true,
|
||||||
@@ -151,16 +170,10 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
type: BillTypeEnum.openapiChat
|
type: BillTypeEnum.openapiChat
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (step === 1) {
|
res.status(500);
|
||||||
// 直接结束流
|
jsonRes(res, {
|
||||||
res.end();
|
code: 500,
|
||||||
console.log('error,结束');
|
error: err
|
||||||
} else {
|
});
|
||||||
res.status(500);
|
|
||||||
jsonRes(res, {
|
|
||||||
code: 500,
|
|
||||||
error: err
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
38
src/pages/api/openapi/chat/lastChatResult.ts
Normal file
38
src/pages/api/openapi/chat/lastChatResult.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { Chat } from '@/service/mongo';
|
||||||
|
import { authUser } from '@/service/utils/auth';
|
||||||
|
import { QuoteItemType } from '../kb/appKbSearch';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
chatId: string;
|
||||||
|
};
|
||||||
|
export type Response = {
|
||||||
|
quote: QuoteItemType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 聊天内容存存储 */
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
const { chatId } = req.query as Props;
|
||||||
|
|
||||||
|
if (!chatId) {
|
||||||
|
throw new Error('缺少参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId } = await authUser({ req });
|
||||||
|
|
||||||
|
const chatItem = await Chat.findOne({ _id: chatId, userId }, { content: { $slice: -1 } });
|
||||||
|
|
||||||
|
jsonRes<Response>(res, {
|
||||||
|
data: {
|
||||||
|
quote: chatItem?.content[0]?.quote || []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
224
src/pages/api/openapi/kb/appKbSearch.ts
Normal file
224
src/pages/api/openapi/kb/appKbSearch.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { authUser } from '@/service/utils/auth';
|
||||||
|
import { PgClient } from '@/service/pg';
|
||||||
|
import { withNextCors } from '@/service/utils/tools';
|
||||||
|
import type { ChatItemSimpleType } from '@/types/chat';
|
||||||
|
import type { ModelSchema } from '@/types/mongoSchema';
|
||||||
|
import { ModelVectorSearchModeEnum } from '@/constants/model';
|
||||||
|
import { authModel } from '@/service/utils/auth';
|
||||||
|
import { ChatModelMap } from '@/constants/model';
|
||||||
|
import { ChatRoleEnum } from '@/constants/chat';
|
||||||
|
import { openaiEmbedding } from '../plugin/openaiEmbedding';
|
||||||
|
import { ModelDataStatusEnum } from '@/constants/model';
|
||||||
|
import { modelToolMap } from '@/utils/plugin';
|
||||||
|
|
||||||
|
export type QuoteItemType = { id: string; q: string; a: string };
|
||||||
|
type Props = {
|
||||||
|
prompts: ChatItemSimpleType[];
|
||||||
|
similarity: number;
|
||||||
|
appId: string;
|
||||||
|
};
|
||||||
|
type Response = {
|
||||||
|
code: 200 | 201;
|
||||||
|
rawSearch: QuoteItemType[];
|
||||||
|
searchPrompts: {
|
||||||
|
obj: ChatRoleEnum;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
|
try {
|
||||||
|
const { userId } = await authUser({ req });
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('userId is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { prompts, similarity, appId } = req.body as Props;
|
||||||
|
|
||||||
|
if (!similarity || !Array.isArray(prompts) || !appId) {
|
||||||
|
throw new Error('params is error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// auth model
|
||||||
|
const { model } = await authModel({
|
||||||
|
modelId: appId,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await appKbSearch({
|
||||||
|
userId,
|
||||||
|
prompts,
|
||||||
|
similarity,
|
||||||
|
model
|
||||||
|
});
|
||||||
|
|
||||||
|
jsonRes<Response>(res, {
|
||||||
|
data: result
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function appKbSearch({
|
||||||
|
model,
|
||||||
|
userId,
|
||||||
|
prompts,
|
||||||
|
similarity
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
prompts: ChatItemSimpleType[];
|
||||||
|
similarity: number;
|
||||||
|
model: ModelSchema;
|
||||||
|
}): Promise<Response> {
|
||||||
|
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
||||||
|
|
||||||
|
// search two times.
|
||||||
|
const userPrompts = prompts.filter((item) => item.obj === 'Human');
|
||||||
|
|
||||||
|
const input: string[] = [
|
||||||
|
userPrompts[userPrompts.length - 1].value,
|
||||||
|
userPrompts[userPrompts.length - 2]?.value
|
||||||
|
].filter((item) => item);
|
||||||
|
|
||||||
|
// get vector
|
||||||
|
const promptVectors = await openaiEmbedding({
|
||||||
|
userId,
|
||||||
|
input
|
||||||
|
});
|
||||||
|
|
||||||
|
// search kb
|
||||||
|
const searchRes = await Promise.all(
|
||||||
|
promptVectors.map((promptVector) =>
|
||||||
|
PgClient.select<{ id: string; q: string; a: string }>('modelData', {
|
||||||
|
fields: ['id', 'q', 'a'],
|
||||||
|
where: [
|
||||||
|
['status', ModelDataStatusEnum.ready],
|
||||||
|
'AND',
|
||||||
|
`kb_id IN (${model.chat.relatedKbs.map((item) => `'${item}'`).join(',')})`,
|
||||||
|
'AND',
|
||||||
|
`vector <=> '[${promptVector}]' < ${similarity}`
|
||||||
|
],
|
||||||
|
order: [{ field: 'vector', mode: `<=> '[${promptVector}]'` }],
|
||||||
|
limit: promptVectors.length === 1 ? 15 : 10
|
||||||
|
}).then((res) => res.rows)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// filter same search result
|
||||||
|
const idSet = new Set<string>();
|
||||||
|
const filterSearch = searchRes.map((search) =>
|
||||||
|
search.filter((item) => {
|
||||||
|
if (idSet.has(item.id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
idSet.add(item.id);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// slice search result by rate.
|
||||||
|
const sliceRateMap: Record<number, number[]> = {
|
||||||
|
1: [1],
|
||||||
|
2: [0.7, 0.3]
|
||||||
|
};
|
||||||
|
const sliceRate = sliceRateMap[searchRes.length] || sliceRateMap[0];
|
||||||
|
// 计算固定提示词的 token 数量
|
||||||
|
const fixedPrompts = [
|
||||||
|
// user system prompt
|
||||||
|
...(model.chat.systemPrompt
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
obj: ChatRoleEnum.System,
|
||||||
|
value: model.chat.systemPrompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: model.chat.searchMode === ModelVectorSearchModeEnum.noContext
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
obj: ChatRoleEnum.System,
|
||||||
|
value: `知识库是关于"${model.name}"的内容,根据知识库内容回答问题.`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
obj: ChatRoleEnum.System,
|
||||||
|
value: `玩一个问答游戏,规则为:
|
||||||
|
1.你完全忘记你已有的知识
|
||||||
|
2.你只回答关于"${model.name}"的问题
|
||||||
|
3.你只从知识库中选择内容进行回答
|
||||||
|
4.如果问题不在知识库中,你会回答:"我不知道。"
|
||||||
|
请务必遵守规则`
|
||||||
|
}
|
||||||
|
])
|
||||||
|
];
|
||||||
|
const fixedSystemTokens = modelToolMap[model.chat.chatModel].countTokens({
|
||||||
|
messages: fixedPrompts
|
||||||
|
});
|
||||||
|
const maxTokens = modelConstantsData.systemMaxToken - fixedSystemTokens;
|
||||||
|
const sliceResult = sliceRate.map((rate, i) =>
|
||||||
|
modelToolMap[model.chat.chatModel]
|
||||||
|
.tokenSlice({
|
||||||
|
maxToken: Math.round(maxTokens * rate),
|
||||||
|
messages: filterSearch[i].map((item) => ({
|
||||||
|
obj: ChatRoleEnum.System,
|
||||||
|
value: `${item.q}\n${item.a}`
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.map((item) => item.value)
|
||||||
|
);
|
||||||
|
|
||||||
|
// slice filterSearch
|
||||||
|
const sliceSearch = filterSearch.map((item, i) => item.slice(0, sliceResult[i].length)).flat();
|
||||||
|
|
||||||
|
// system prompt
|
||||||
|
const systemPrompt = sliceResult.flat().join('\n').trim();
|
||||||
|
|
||||||
|
/* 高相似度+不回复 */
|
||||||
|
if (!systemPrompt && model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity) {
|
||||||
|
return {
|
||||||
|
code: 201,
|
||||||
|
rawSearch: [],
|
||||||
|
searchPrompts: [
|
||||||
|
{
|
||||||
|
obj: ChatRoleEnum.System,
|
||||||
|
value: '对不起,你的问题不在知识库中。'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/* 高相似度+无上下文,不添加额外知识,仅用系统提示词 */
|
||||||
|
if (!systemPrompt && model.chat.searchMode === ModelVectorSearchModeEnum.noContext) {
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
rawSearch: [],
|
||||||
|
searchPrompts: model.chat.systemPrompt
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
obj: ChatRoleEnum.System,
|
||||||
|
value: model.chat.systemPrompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
rawSearch: sliceSearch,
|
||||||
|
searchPrompts: [
|
||||||
|
{
|
||||||
|
obj: ChatRoleEnum.System,
|
||||||
|
value: `知识库:${systemPrompt}`
|
||||||
|
},
|
||||||
|
...fixedPrompts
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
77
src/pages/api/openapi/plugin/openaiEmbedding.ts
Normal file
77
src/pages/api/openapi/plugin/openaiEmbedding.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { authUser } from '@/service/utils/auth';
|
||||||
|
import { PgClient } from '@/service/pg';
|
||||||
|
import { withNextCors } from '@/service/utils/tools';
|
||||||
|
import { getApiKey } from '@/service/utils/auth';
|
||||||
|
import { getOpenAIApi } from '@/service/utils/chat/openai';
|
||||||
|
import { embeddingModel } from '@/constants/model';
|
||||||
|
import { axiosConfig } from '@/service/utils/tools';
|
||||||
|
import { pushGenerateVectorBill } from '@/service/events/pushBill';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
input: string[];
|
||||||
|
};
|
||||||
|
type Response = number[][];
|
||||||
|
|
||||||
|
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
|
try {
|
||||||
|
const { userId } = await authUser({ req });
|
||||||
|
let { input } = req.query as Props;
|
||||||
|
|
||||||
|
if (!Array.isArray(input)) {
|
||||||
|
throw new Error('缺少参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonRes<Response>(res, {
|
||||||
|
data: await openaiEmbedding({ userId, input, mustPay: true })
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function openaiEmbedding({
|
||||||
|
userId,
|
||||||
|
input,
|
||||||
|
mustPay = false
|
||||||
|
}: { userId: string; mustPay?: boolean } & Props) {
|
||||||
|
const { userOpenAiKey, systemAuthKey } = await getApiKey({
|
||||||
|
model: 'gpt-3.5-turbo',
|
||||||
|
userId,
|
||||||
|
mustPay
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取 chatAPI
|
||||||
|
const chatAPI = getOpenAIApi();
|
||||||
|
|
||||||
|
// 把输入的内容转成向量
|
||||||
|
const result = await chatAPI
|
||||||
|
.createEmbedding(
|
||||||
|
{
|
||||||
|
model: embeddingModel,
|
||||||
|
input
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: 60000,
|
||||||
|
...axiosConfig(userOpenAiKey || systemAuthKey)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => ({
|
||||||
|
tokenLen: res.data.usage.total_tokens || 0,
|
||||||
|
vectors: res.data.data.map((item) => item.embedding)
|
||||||
|
}));
|
||||||
|
|
||||||
|
pushGenerateVectorBill({
|
||||||
|
isPay: !userOpenAiKey,
|
||||||
|
userId,
|
||||||
|
text: input.join(''),
|
||||||
|
tokenLen: result.tokenLen
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.vectors;
|
||||||
|
}
|
119
src/pages/api/openapi/text/gptMessagesSlice.ts
Normal file
119
src/pages/api/openapi/text/gptMessagesSlice.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { type Tiktoken } from '@dqbd/tiktoken';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { authUser } from '@/service/utils/auth';
|
||||||
|
import Graphemer from 'graphemer';
|
||||||
|
import type { ChatItemSimpleType } from '@/types/chat';
|
||||||
|
import { ChatCompletionRequestMessage } from 'openai';
|
||||||
|
import { getOpenAiEncMap } from '@/utils/plugin/openai';
|
||||||
|
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||||
|
|
||||||
|
type ModelType = 'gpt-3.5-turbo' | 'gpt-4' | 'gpt-4-32k';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
messages: ChatItemSimpleType[];
|
||||||
|
model: ModelType;
|
||||||
|
maxLen: number;
|
||||||
|
};
|
||||||
|
type Response = ChatItemSimpleType[];
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
await authUser({ req });
|
||||||
|
|
||||||
|
const { messages, model, maxLen } = req.body as Props;
|
||||||
|
|
||||||
|
if (!Array.isArray(messages) || !model || !maxLen) {
|
||||||
|
throw new Error('params is error');
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonRes<Response>(res, {
|
||||||
|
data: gpt_chatItemTokenSlice({
|
||||||
|
messages,
|
||||||
|
model,
|
||||||
|
maxToken: maxLen
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gpt_chatItemTokenSlice({
|
||||||
|
messages,
|
||||||
|
model,
|
||||||
|
maxToken
|
||||||
|
}: {
|
||||||
|
messages: ChatItemSimpleType[];
|
||||||
|
model: ModelType;
|
||||||
|
maxToken: number;
|
||||||
|
}) {
|
||||||
|
const textDecoder = new TextDecoder();
|
||||||
|
const graphemer = new Graphemer();
|
||||||
|
|
||||||
|
function getChatGPTEncodingText(messages: ChatCompletionRequestMessage[], model: ModelType) {
|
||||||
|
const isGpt3 = model === 'gpt-3.5-turbo';
|
||||||
|
|
||||||
|
const msgSep = isGpt3 ? '\n' : '';
|
||||||
|
const roleSep = isGpt3 ? '\n' : '<|im_sep|>';
|
||||||
|
|
||||||
|
return [
|
||||||
|
messages
|
||||||
|
.map(({ name = '', role, content }) => {
|
||||||
|
return `<|im_start|>${name || role}${roleSep}${content}<|im_end|>`;
|
||||||
|
})
|
||||||
|
.join(msgSep),
|
||||||
|
`<|im_start|>assistant${roleSep}`
|
||||||
|
].join(msgSep);
|
||||||
|
}
|
||||||
|
function text2TokensLen(encoder: Tiktoken, inputText: string) {
|
||||||
|
const encoding = encoder.encode(inputText, 'all');
|
||||||
|
const segments: { text: string; tokens: { id: number; idx: number }[] }[] = [];
|
||||||
|
|
||||||
|
let byteAcc: number[] = [];
|
||||||
|
let tokenAcc: { id: number; idx: number }[] = [];
|
||||||
|
let inputGraphemes = graphemer.splitGraphemes(inputText);
|
||||||
|
|
||||||
|
for (let idx = 0; idx < encoding.length; idx++) {
|
||||||
|
const token = encoding[idx]!;
|
||||||
|
byteAcc.push(...encoder.decode_single_token_bytes(token));
|
||||||
|
tokenAcc.push({ id: token, idx });
|
||||||
|
|
||||||
|
const segmentText = textDecoder.decode(new Uint8Array(byteAcc));
|
||||||
|
const graphemes = graphemer.splitGraphemes(segmentText);
|
||||||
|
|
||||||
|
if (graphemes.every((item, idx) => inputGraphemes[idx] === item)) {
|
||||||
|
segments.push({ text: segmentText, tokens: tokenAcc });
|
||||||
|
|
||||||
|
byteAcc = [];
|
||||||
|
tokenAcc = [];
|
||||||
|
inputGraphemes = inputGraphemes.slice(graphemes.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.reduce((memo, i) => memo + i.tokens.length, 0) ?? 0;
|
||||||
|
}
|
||||||
|
const OpenAiEncMap = getOpenAiEncMap();
|
||||||
|
const enc = OpenAiEncMap[model];
|
||||||
|
|
||||||
|
let result: ChatItemSimpleType[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < messages.length; i++) {
|
||||||
|
const msgs = [...result, messages[i]];
|
||||||
|
const tokens = text2TokensLen(
|
||||||
|
enc,
|
||||||
|
getChatGPTEncodingText(adaptChatItem_openAI({ messages }), model)
|
||||||
|
);
|
||||||
|
if (tokens < maxToken) {
|
||||||
|
result = msgs;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
@@ -3,10 +3,10 @@ import { useRouter } from 'next/router';
|
|||||||
import {
|
import {
|
||||||
getInitChatSiteInfo,
|
getInitChatSiteInfo,
|
||||||
delChatRecordByIndex,
|
delChatRecordByIndex,
|
||||||
postSaveChat,
|
getChatResult,
|
||||||
delChatHistoryById
|
delChatHistoryById
|
||||||
} from '@/api/chat';
|
} from '@/api/chat';
|
||||||
import type { ChatSiteItemType, ExportChatType } from '@/types/chat';
|
import type { ChatItemType, ChatSiteItemType, ExportChatType } from '@/types/chat';
|
||||||
import {
|
import {
|
||||||
Textarea,
|
Textarea,
|
||||||
Box,
|
Box,
|
||||||
@@ -29,13 +29,14 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
useOutsideClick,
|
useOutsideClick,
|
||||||
useTheme
|
useTheme,
|
||||||
|
ModalHeader
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useGlobalStore } from '@/store/global';
|
import { useGlobalStore } from '@/store/global';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { useCopyData, voiceBroadcast, hasVoiceApi } from '@/utils/tools';
|
import { useCopyData, voiceBroadcast, hasVoiceApi, delay } from '@/utils/tools';
|
||||||
import { streamFetch } from '@/api/fetch';
|
import { streamFetch } from '@/api/fetch';
|
||||||
import MyIcon from '@/components/Icon';
|
import MyIcon from '@/components/Icon';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
@@ -47,6 +48,7 @@ import { useLoading } from '@/hooks/useLoading';
|
|||||||
import { fileDownload } from '@/utils/file';
|
import { fileDownload } from '@/utils/file';
|
||||||
import { htmlTemplate } from '@/constants/common';
|
import { htmlTemplate } from '@/constants/common';
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
|
import type { QuoteItemType } from '@/pages/api/openapi/kb/appKbSearch';
|
||||||
import Loading from '@/components/Loading';
|
import Loading from '@/components/Loading';
|
||||||
import Markdown from '@/components/Markdown';
|
import Markdown from '@/components/Markdown';
|
||||||
import SideBar from '@/components/SideBar';
|
import SideBar from '@/components/SideBar';
|
||||||
@@ -78,7 +80,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
|||||||
const controller = useRef(new AbortController());
|
const controller = useRef(new AbortController());
|
||||||
const isLeavePage = useRef(false);
|
const isLeavePage = useRef(false);
|
||||||
|
|
||||||
const [showSystemPrompt, setShowSystemPrompt] = useState('');
|
const [showQuote, setShowQuote] = useState<QuoteItemType[]>([]);
|
||||||
const [messageContextMenuData, setMessageContextMenuData] = useState<{
|
const [messageContextMenuData, setMessageContextMenuData] = useState<{
|
||||||
// message messageContextMenuData
|
// message messageContextMenuData
|
||||||
left: number;
|
left: number;
|
||||||
@@ -173,13 +175,14 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
|||||||
controller.current = abortSignal;
|
controller.current = abortSignal;
|
||||||
isLeavePage.current = false;
|
isLeavePage.current = false;
|
||||||
|
|
||||||
const prompt = {
|
const prompt: ChatItemType[] = prompts.map((item) => ({
|
||||||
obj: prompts[0].obj,
|
_id: item._id,
|
||||||
value: prompts[0].value
|
obj: item.obj,
|
||||||
};
|
value: item.value
|
||||||
|
}));
|
||||||
|
|
||||||
// 流请求,获取数据
|
// 流请求,获取数据
|
||||||
let { responseText, systemPrompt, newChatId } = await streamFetch({
|
const { newChatId } = await streamFetch({
|
||||||
url: '/api/chat/chat',
|
url: '/api/chat/chat',
|
||||||
data: {
|
data: {
|
||||||
prompt,
|
prompt,
|
||||||
@@ -207,39 +210,16 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// save chat record
|
if (newChatId) {
|
||||||
try {
|
setForbidLoadChatData(true);
|
||||||
newChatId = await postSaveChat({
|
router.replace(`/chat?modelId=${modelId}&chatId=${newChatId}`);
|
||||||
newChatId, // 如果有newChatId,会自动以这个Id创建对话框
|
|
||||||
modelId,
|
|
||||||
chatId,
|
|
||||||
prompts: [
|
|
||||||
{
|
|
||||||
_id: prompts[0]._id,
|
|
||||||
obj: 'Human',
|
|
||||||
value: prompt.value
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: prompts[1]._id,
|
|
||||||
obj: 'AI',
|
|
||||||
value: responseText,
|
|
||||||
systemPrompt
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
if (newChatId) {
|
|
||||||
setForbidLoadChatData(true);
|
|
||||||
router.replace(`/chat?modelId=${modelId}&chatId=${newChatId}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: '对话出现异常, 继续对话会导致上下文丢失,请刷新页面',
|
|
||||||
status: 'warning',
|
|
||||||
duration: 3000,
|
|
||||||
isClosable: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abortSignal.signal.aborted && (await delay(600));
|
||||||
|
|
||||||
|
// get chat result
|
||||||
|
const { quote } = await getChatResult(chatId || newChatId);
|
||||||
|
|
||||||
// 设置聊天内容为完成状态
|
// 设置聊天内容为完成状态
|
||||||
setChatData((state) => ({
|
setChatData((state) => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -249,7 +229,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
|||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
status: 'finish',
|
status: 'finish',
|
||||||
systemPrompt
|
quote
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
@@ -260,16 +240,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
|||||||
generatingMessage();
|
generatingMessage();
|
||||||
}, 100);
|
}, 100);
|
||||||
},
|
},
|
||||||
[
|
[chatId, setForbidLoadChatData, generatingMessage, loadHistory, modelId, router, setChatData]
|
||||||
chatId,
|
|
||||||
setForbidLoadChatData,
|
|
||||||
generatingMessage,
|
|
||||||
loadHistory,
|
|
||||||
modelId,
|
|
||||||
router,
|
|
||||||
setChatData,
|
|
||||||
toast
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -717,24 +688,24 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
|||||||
{item.obj === 'Human' && <Box flex={1} />}
|
{item.obj === 'Human' && <Box flex={1} />}
|
||||||
{/* avatar */}
|
{/* avatar */}
|
||||||
<Menu autoSelect={false} isLazy>
|
<Menu autoSelect={false} isLazy>
|
||||||
<MenuButton
|
<Tooltip label={item.obj === 'AI' ? '应用详情' : ''}>
|
||||||
as={Box}
|
<MenuButton
|
||||||
{...(item.obj === 'AI'
|
as={Box}
|
||||||
? {
|
{...(item.obj === 'AI'
|
||||||
order: 1,
|
? {
|
||||||
mr: ['6px', 2],
|
order: 1,
|
||||||
cursor: 'pointer',
|
mr: ['6px', 2],
|
||||||
onClick: () =>
|
cursor: 'pointer',
|
||||||
isPc &&
|
onClick: () =>
|
||||||
chatData.model.canUse &&
|
isPc &&
|
||||||
router.push(`/model?modelId=${chatData.modelId}`)
|
chatData.model.canUse &&
|
||||||
}
|
router.push(`/model?modelId=${chatData.modelId}`)
|
||||||
: {
|
}
|
||||||
order: 3,
|
: {
|
||||||
ml: ['6px', 2]
|
order: 3,
|
||||||
})}
|
ml: ['6px', 2]
|
||||||
>
|
})}
|
||||||
<Tooltip label={item.obj === 'AI' ? '应用详情' : ''}>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={
|
src={
|
||||||
item.obj === 'Human'
|
item.obj === 'Human'
|
||||||
@@ -744,8 +715,8 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
|||||||
w={['20px', '34px']}
|
w={['20px', '34px']}
|
||||||
h={['20px', '34px']}
|
h={['20px', '34px']}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</MenuButton>
|
||||||
</MenuButton>
|
</Tooltip>
|
||||||
{!isPc && <RenderContextMenu history={item} index={index} AiDetail />}
|
{!isPc && <RenderContextMenu history={item} index={index} AiDetail />}
|
||||||
</Menu>
|
</Menu>
|
||||||
{/* message */}
|
{/* message */}
|
||||||
@@ -764,7 +735,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
|||||||
isChatting={isChatting && index === chatData.history.length - 1}
|
isChatting={isChatting && index === chatData.history.length - 1}
|
||||||
formatLink
|
formatLink
|
||||||
/>
|
/>
|
||||||
{item.systemPrompt && (
|
{item.quote && item.quote.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
size={'xs'}
|
size={'xs'}
|
||||||
mt={2}
|
mt={2}
|
||||||
@@ -772,9 +743,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
|||||||
colorScheme={'gray'}
|
colorScheme={'gray'}
|
||||||
variant={'outline'}
|
variant={'outline'}
|
||||||
w={'90px'}
|
w={'90px'}
|
||||||
onClick={() => setShowSystemPrompt(item.systemPrompt || '')}
|
onClick={() => setShowQuote(item.quote || [])}
|
||||||
>
|
>
|
||||||
查看提示词
|
查看引用
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -907,12 +878,24 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
|||||||
)}
|
)}
|
||||||
{/* system prompt show modal */}
|
{/* system prompt show modal */}
|
||||||
{
|
{
|
||||||
<Modal isOpen={!!showSystemPrompt} onClose={() => setShowSystemPrompt('')}>
|
<Modal isOpen={showQuote.length > 0} onClose={() => setShowQuote([])}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent pt={5} maxW={'min(90vw, 600px)'} h={'80vh'} overflow={'overlay'}>
|
<ModalContent maxW={'min(90vw, 700px)'} h={'80vh'} overflow={'overlay'}>
|
||||||
|
<ModalHeader>知识库引用({showQuote.length}条)</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody pt={5} whiteSpace={'pre-wrap'} textAlign={'justify'}>
|
<ModalBody whiteSpace={'pre-wrap'} textAlign={'justify'} fontSize={'sm'}>
|
||||||
{showSystemPrompt}
|
{showQuote.map((item) => (
|
||||||
|
<Box
|
||||||
|
key={item.id}
|
||||||
|
p={2}
|
||||||
|
borderRadius={'sm'}
|
||||||
|
border={theme.borders.base}
|
||||||
|
_notLast={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
<Box>{item.q}</Box>
|
||||||
|
<Box>{item.a}</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@@ -73,7 +73,6 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
|
|||||||
const isLeavePage = useRef(false);
|
const isLeavePage = useRef(false);
|
||||||
|
|
||||||
const [inputVal, setInputVal] = useState(''); // user input prompt
|
const [inputVal, setInputVal] = useState(''); // user input prompt
|
||||||
const [showSystemPrompt, setShowSystemPrompt] = useState('');
|
|
||||||
const [messageContextMenuData, setMessageContextMenuData] = useState<{
|
const [messageContextMenuData, setMessageContextMenuData] = useState<{
|
||||||
// message messageContextMenuData
|
// message messageContextMenuData
|
||||||
left: number;
|
left: number;
|
||||||
@@ -178,7 +177,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// 流请求,获取数据
|
// 流请求,获取数据
|
||||||
const { responseText, systemPrompt } = await streamFetch({
|
const { responseText } = await streamFetch({
|
||||||
url: '/api/chat/shareChat/chat',
|
url: '/api/chat/shareChat/chat',
|
||||||
data: {
|
data: {
|
||||||
prompts: formatPrompts.slice(-shareChatData.maxContext - 1, -1),
|
prompts: formatPrompts.slice(-shareChatData.maxContext - 1, -1),
|
||||||
@@ -215,8 +214,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
|
|||||||
if (index !== state.history.length - 1) return item;
|
if (index !== state.history.length - 1) return item;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
status: 'finish',
|
status: 'finish'
|
||||||
systemPrompt
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -614,19 +612,19 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
|
|||||||
{item.obj === 'Human' && <Box flex={1} />}
|
{item.obj === 'Human' && <Box flex={1} />}
|
||||||
{/* avatar */}
|
{/* avatar */}
|
||||||
<Menu autoSelect={false} isLazy>
|
<Menu autoSelect={false} isLazy>
|
||||||
<MenuButton
|
<Tooltip label={item.obj === 'AI' ? '应用详情' : ''}>
|
||||||
as={Box}
|
<MenuButton
|
||||||
{...(item.obj === 'AI'
|
as={Box}
|
||||||
? {
|
{...(item.obj === 'AI'
|
||||||
order: 1,
|
? {
|
||||||
mr: ['6px', 2]
|
order: 1,
|
||||||
}
|
mr: ['6px', 2]
|
||||||
: {
|
}
|
||||||
order: 3,
|
: {
|
||||||
ml: ['6px', 2]
|
order: 3,
|
||||||
})}
|
ml: ['6px', 2]
|
||||||
>
|
})}
|
||||||
<Tooltip label={item.obj === 'AI' ? '应用详情' : ''}>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={
|
src={
|
||||||
item.obj === 'Human'
|
item.obj === 'Human'
|
||||||
@@ -636,8 +634,8 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
|
|||||||
w={['20px', '34px']}
|
w={['20px', '34px']}
|
||||||
h={['20px', '34px']}
|
h={['20px', '34px']}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</MenuButton>
|
||||||
</MenuButton>
|
</Tooltip>
|
||||||
{!isPc && <RenderContextMenu history={item} index={index} />}
|
{!isPc && <RenderContextMenu history={item} index={index} />}
|
||||||
</Menu>
|
</Menu>
|
||||||
{/* message */}
|
{/* message */}
|
||||||
@@ -656,19 +654,6 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
|
|||||||
isChatting={isChatting && index === shareChatData.history.length - 1}
|
isChatting={isChatting && index === shareChatData.history.length - 1}
|
||||||
formatLink
|
formatLink
|
||||||
/>
|
/>
|
||||||
{item.systemPrompt && (
|
|
||||||
<Button
|
|
||||||
size={'xs'}
|
|
||||||
mt={2}
|
|
||||||
fontWeight={'normal'}
|
|
||||||
colorScheme={'gray'}
|
|
||||||
variant={'outline'}
|
|
||||||
w={'90px'}
|
|
||||||
onClick={() => setShowSystemPrompt(item.systemPrompt || '')}
|
|
||||||
>
|
|
||||||
查看提示词
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
@@ -796,18 +781,6 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
|
|||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)}
|
)}
|
||||||
{/* system prompt show modal */}
|
|
||||||
{
|
|
||||||
<Modal isOpen={!!showSystemPrompt} onClose={() => setShowSystemPrompt('')}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent maxW={'min(90vw, 600px)'} pr={2} maxH={'80vh'} overflowY={'auto'}>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody pt={5} whiteSpace={'pre-wrap'} textAlign={'justify'}>
|
|
||||||
{showSystemPrompt}
|
|
||||||
</ModalBody>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
}
|
|
||||||
{/* context menu */}
|
{/* context menu */}
|
||||||
{messageContextMenuData && (
|
{messageContextMenuData && (
|
||||||
<Box
|
<Box
|
||||||
|
@@ -358,7 +358,9 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
|||||||
rows={8}
|
rows={8}
|
||||||
maxLength={-1}
|
maxLength={-1}
|
||||||
isDisabled={!isOwner}
|
isDisabled={!isOwner}
|
||||||
placeholder={'模型默认的 prompt 词,通过调整该内容,可以引导模型聊天方向。'}
|
placeholder={
|
||||||
|
'模型默认的 prompt 词,通过调整该内容,可以引导模型聊天方向。\n\n如果使用了知识库搜索,没有填写该内容时,系统会自动补充提示词;如果填写了内容,则以填写的内容为准。'
|
||||||
|
}
|
||||||
{...register('chat.systemPrompt')}
|
{...register('chat.systemPrompt')}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { openaiCreateEmbedding } from '../utils/chat/openai';
|
|
||||||
import { getApiKey } from '../utils/auth';
|
import { getApiKey } from '../utils/auth';
|
||||||
import { openaiError2 } from '../errorCode';
|
import { openaiError2 } from '../errorCode';
|
||||||
import { PgClient } from '@/service/pg';
|
import { PgClient } from '@/service/pg';
|
||||||
import { getErrText } from '@/utils/tools';
|
import { getErrText } from '@/utils/tools';
|
||||||
|
import { openaiEmbedding } from '@/pages/api/openapi/plugin/openaiEmbedding';
|
||||||
|
|
||||||
export async function generateVector(next = false): Promise<any> {
|
export async function generateVector(next = false): Promise<any> {
|
||||||
if (process.env.queueTask !== '1') {
|
if (process.env.queueTask !== '1') {
|
||||||
@@ -42,24 +42,20 @@ export async function generateVector(next = false): Promise<any> {
|
|||||||
dataId = dataItem.id;
|
dataId = dataItem.id;
|
||||||
|
|
||||||
// 获取 openapi Key
|
// 获取 openapi Key
|
||||||
let userOpenAiKey;
|
|
||||||
try {
|
try {
|
||||||
const res = await getApiKey({ model: 'gpt-3.5-turbo', userId: dataItem.userId });
|
await getApiKey({ model: 'gpt-3.5-turbo', userId: dataItem.userId });
|
||||||
userOpenAiKey = res.userOpenAiKey;
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await PgClient.delete('modelData', {
|
await PgClient.delete('modelData', {
|
||||||
where: [['id', dataId]]
|
where: [['id', dataId]]
|
||||||
});
|
});
|
||||||
generateVector(true);
|
|
||||||
getErrText(err, '获取 OpenAi Key 失败');
|
getErrText(err, '获取 OpenAi Key 失败');
|
||||||
return;
|
return generateVector(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成词向量
|
// 生成词向量
|
||||||
const { vectors } = await openaiCreateEmbedding({
|
const vectors = await openaiEmbedding({
|
||||||
textArr: [dataItem.q],
|
input: [dataItem.q],
|
||||||
userId: dataItem.userId,
|
userId: dataItem.userId
|
||||||
userOpenAiKey
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新 pg 向量和状态数据
|
// 更新 pg 向量和状态数据
|
||||||
|
@@ -47,10 +47,14 @@ const ChatSchema = new Schema({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
systemPrompt: {
|
quote: {
|
||||||
type: String,
|
type: [{ id: String, q: String, a: String }],
|
||||||
default: ''
|
default: []
|
||||||
}
|
}
|
||||||
|
// systemPrompt: {
|
||||||
|
// type: String,
|
||||||
|
// default: ''
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
default: []
|
default: []
|
||||||
|
@@ -1,175 +0,0 @@
|
|||||||
import { PgClient } from '@/service/pg';
|
|
||||||
import { ModelDataStatusEnum, ModelVectorSearchModeEnum, ChatModelMap } from '@/constants/model';
|
|
||||||
import { ModelSchema } from '@/types/mongoSchema';
|
|
||||||
import { openaiCreateEmbedding } from '../utils/chat/openai';
|
|
||||||
import { ChatRoleEnum } from '@/constants/chat';
|
|
||||||
import { modelToolMap } from '@/utils/chat';
|
|
||||||
import { ChatItemSimpleType } from '@/types/chat';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* use openai embedding search kb
|
|
||||||
*/
|
|
||||||
export const searchKb = async ({
|
|
||||||
userOpenAiKey,
|
|
||||||
prompts,
|
|
||||||
similarity = 0.2,
|
|
||||||
model,
|
|
||||||
userId
|
|
||||||
}: {
|
|
||||||
userOpenAiKey?: string;
|
|
||||||
prompts: ChatItemSimpleType[];
|
|
||||||
model: ModelSchema;
|
|
||||||
userId: string;
|
|
||||||
similarity?: number;
|
|
||||||
}): Promise<{
|
|
||||||
code: 200 | 201;
|
|
||||||
searchPrompts: {
|
|
||||||
obj: ChatRoleEnum;
|
|
||||||
value: string;
|
|
||||||
}[];
|
|
||||||
}> => {
|
|
||||||
async function search(textArr: string[] = []) {
|
|
||||||
const limitMap: Record<ModelVectorSearchModeEnum, number> = {
|
|
||||||
[ModelVectorSearchModeEnum.hightSimilarity]: 15,
|
|
||||||
[ModelVectorSearchModeEnum.noContext]: 15,
|
|
||||||
[ModelVectorSearchModeEnum.lowSimilarity]: 20
|
|
||||||
};
|
|
||||||
// 获取提示词的向量
|
|
||||||
const { vectors: promptVectors } = await openaiCreateEmbedding({
|
|
||||||
userOpenAiKey,
|
|
||||||
userId,
|
|
||||||
textArr
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchRes = await Promise.all(
|
|
||||||
promptVectors.map((promptVector) =>
|
|
||||||
PgClient.select<{ id: string; q: string; a: string }>('modelData', {
|
|
||||||
fields: ['id', 'q', 'a'],
|
|
||||||
where: [
|
|
||||||
['status', ModelDataStatusEnum.ready],
|
|
||||||
'AND',
|
|
||||||
`kb_id IN (${model.chat.relatedKbs.map((item) => `'${item}'`).join(',')})`,
|
|
||||||
'AND',
|
|
||||||
`vector <=> '[${promptVector}]' < ${similarity}`
|
|
||||||
],
|
|
||||||
order: [{ field: 'vector', mode: `<=> '[${promptVector}]'` }],
|
|
||||||
limit: limitMap[model.chat.searchMode]
|
|
||||||
}).then((res) => res.rows)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove repeat record
|
|
||||||
const idSet = new Set<string>();
|
|
||||||
const filterSearch = searchRes.map((search) =>
|
|
||||||
search.filter((item) => {
|
|
||||||
if (idSet.has(item.id)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
idSet.add(item.id);
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return filterSearch.map((item) => item.map((item) => `${item.q}\n${item.a}`).join('\n'));
|
|
||||||
}
|
|
||||||
const modelConstantsData = ChatModelMap[model.chat.chatModel];
|
|
||||||
|
|
||||||
// search three times
|
|
||||||
const userPrompts = prompts.filter((item) => item.obj === 'Human');
|
|
||||||
|
|
||||||
const searchArr: string[] = [
|
|
||||||
userPrompts[userPrompts.length - 1].value,
|
|
||||||
userPrompts[userPrompts.length - 2]?.value
|
|
||||||
].filter((item) => item);
|
|
||||||
const systemPrompts = await search(searchArr);
|
|
||||||
|
|
||||||
// filter system prompts.
|
|
||||||
const filterRateMap: Record<number, number[]> = {
|
|
||||||
1: [1],
|
|
||||||
2: [0.7, 0.3]
|
|
||||||
};
|
|
||||||
const filterRate = filterRateMap[systemPrompts.length] || filterRateMap[0];
|
|
||||||
|
|
||||||
// 计算固定提示词的 token 数量
|
|
||||||
const fixedPrompts = [
|
|
||||||
...(model.chat.systemPrompt
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
obj: ChatRoleEnum.System,
|
|
||||||
value: model.chat.systemPrompt
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(model.chat.searchMode === ModelVectorSearchModeEnum.noContext
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
obj: ChatRoleEnum.System,
|
|
||||||
value: `知识库是关于"${model.name}"的内容,根据知识库内容回答问题.`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
obj: ChatRoleEnum.System,
|
|
||||||
value: `玩一个问答游戏,规则为:
|
|
||||||
1.你完全忘记你已有的知识
|
|
||||||
2.你只回答关于"${model.name}"的问题
|
|
||||||
3.你只从知识库中选择内容进行回答
|
|
||||||
4.如果问题不在知识库中,你会回答:"我不知道。"
|
|
||||||
请务必遵守规则`
|
|
||||||
}
|
|
||||||
])
|
|
||||||
];
|
|
||||||
const fixedSystemTokens = modelToolMap[model.chat.chatModel].countTokens({
|
|
||||||
messages: fixedPrompts
|
|
||||||
});
|
|
||||||
const maxTokens = modelConstantsData.systemMaxToken - fixedSystemTokens;
|
|
||||||
|
|
||||||
const filterSystemPrompt = filterRate
|
|
||||||
.map((rate, i) =>
|
|
||||||
modelToolMap[model.chat.chatModel].sliceText({
|
|
||||||
text: systemPrompts[i],
|
|
||||||
length: Math.floor(maxTokens * rate)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.join('\n')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
/* 高相似度+不回复 */
|
|
||||||
if (!filterSystemPrompt && model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity) {
|
|
||||||
return {
|
|
||||||
code: 201,
|
|
||||||
searchPrompts: [
|
|
||||||
{
|
|
||||||
obj: ChatRoleEnum.System,
|
|
||||||
value: '对不起,你的问题不在知识库中。'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/* 高相似度+无上下文,不添加额外知识,仅用系统提示词 */
|
|
||||||
if (!filterSystemPrompt && model.chat.searchMode === ModelVectorSearchModeEnum.noContext) {
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
searchPrompts: model.chat.systemPrompt
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
obj: ChatRoleEnum.System,
|
|
||||||
value: model.chat.systemPrompt
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 有匹配 */
|
|
||||||
return {
|
|
||||||
code: 200,
|
|
||||||
searchPrompts: [
|
|
||||||
{
|
|
||||||
obj: ChatRoleEnum.System,
|
|
||||||
value: `知识库:${filterSystemPrompt}`
|
|
||||||
},
|
|
||||||
...fixedPrompts
|
|
||||||
]
|
|
||||||
};
|
|
||||||
};
|
|
@@ -38,12 +38,14 @@ export const authUser = async ({
|
|||||||
req,
|
req,
|
||||||
authToken = false,
|
authToken = false,
|
||||||
authOpenApi = false,
|
authOpenApi = false,
|
||||||
authRoot = false
|
authRoot = false,
|
||||||
|
authBalance = false
|
||||||
}: {
|
}: {
|
||||||
req: NextApiRequest;
|
req: NextApiRequest;
|
||||||
authToken?: boolean;
|
authToken?: boolean;
|
||||||
authOpenApi?: boolean;
|
authOpenApi?: boolean;
|
||||||
authRoot?: boolean;
|
authRoot?: boolean;
|
||||||
|
authBalance?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const parseOpenApiKey = async (apiKey?: string) => {
|
const parseOpenApiKey = async (apiKey?: string) => {
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
@@ -99,6 +101,17 @@ export const authUser = async ({
|
|||||||
return Promise.reject(ERROR_ENUM.unAuthorization);
|
return Promise.reject(ERROR_ENUM.unAuthorization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authBalance) {
|
||||||
|
const user = await User.findById(uid);
|
||||||
|
if (!user) {
|
||||||
|
return Promise.reject(ERROR_ENUM.unAuthorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.openaiKey && formatPrice(user.balance) <= 0) {
|
||||||
|
return Promise.reject(ERROR_ENUM.insufficientQuota);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId: uid
|
userId: uid
|
||||||
};
|
};
|
||||||
@@ -226,7 +239,7 @@ export const authChat = async ({
|
|||||||
req
|
req
|
||||||
}: {
|
}: {
|
||||||
modelId: string;
|
modelId: string;
|
||||||
chatId: '' | string;
|
chatId?: string;
|
||||||
req: NextApiRequest;
|
req: NextApiRequest;
|
||||||
}) => {
|
}) => {
|
||||||
const { userId } = await authUser({ req, authToken: true });
|
const { userId } = await authUser({ req, authToken: true });
|
||||||
|
@@ -1,17 +1,9 @@
|
|||||||
import { ChatCompletionType, StreamResponseType } from './index';
|
import { ChatCompletionType, StreamResponseType } from './index';
|
||||||
import { ChatRoleEnum } from '@/constants/chat';
|
import { ChatRoleEnum } from '@/constants/chat';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import mongoose from 'mongoose';
|
|
||||||
import { NEW_CHATID_HEADER } from '@/constants/chat';
|
|
||||||
|
|
||||||
/* 模型对话 */
|
/* 模型对话 */
|
||||||
export const claudChat = async ({ apiKey, messages, stream, chatId, res }: ChatCompletionType) => {
|
export const claudChat = async ({ apiKey, messages, stream, chatId }: ChatCompletionType) => {
|
||||||
const conversationId = chatId || String(new mongoose.Types.ObjectId());
|
|
||||||
// create a new chat
|
|
||||||
!chatId &&
|
|
||||||
messages.filter((item) => item.obj === 'Human').length === 1 &&
|
|
||||||
res?.setHeader(NEW_CHATID_HEADER, conversationId);
|
|
||||||
|
|
||||||
// get system prompt
|
// get system prompt
|
||||||
const systemPrompt = messages
|
const systemPrompt = messages
|
||||||
.filter((item) => item.obj === 'System')
|
.filter((item) => item.obj === 'System')
|
||||||
@@ -26,7 +18,7 @@ export const claudChat = async ({ apiKey, messages, stream, chatId, res }: ChatC
|
|||||||
{
|
{
|
||||||
prompt,
|
prompt,
|
||||||
stream,
|
stream,
|
||||||
conversationId
|
conversationId: chatId
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@@ -55,8 +47,7 @@ export const claudStreamResponse = async ({ res, chatResponse, prompts }: Stream
|
|||||||
try {
|
try {
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
for await (const chunk of chatResponse.data as any) {
|
for await (const chunk of chatResponse.data as any) {
|
||||||
if (!res.writable) {
|
if (res.closed) {
|
||||||
// 流被中断了,直接忽略后面的内容
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const content = decoder.decode(chunk);
|
const content = decoder.decode(chunk);
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ChatItemSimpleType } from '@/types/chat';
|
import { ChatItemSimpleType } from '@/types/chat';
|
||||||
import { modelToolMap } from '@/utils/chat';
|
import { modelToolMap } from '@/utils/plugin';
|
||||||
import type { ChatModelType } from '@/constants/model';
|
import type { ChatModelType } from '@/constants/model';
|
||||||
import { ChatRoleEnum, SYSTEM_PROMPT_HEADER } from '@/constants/chat';
|
import { ChatRoleEnum } from '@/constants/chat';
|
||||||
import { OpenAiChatEnum, ClaudeEnum } from '@/constants/model';
|
import { OpenAiChatEnum, ClaudeEnum } from '@/constants/model';
|
||||||
import { chatResponse, openAiStreamResponse } from './openai';
|
import { chatResponse, openAiStreamResponse } from './openai';
|
||||||
import { claudChat, claudStreamResponse } from './claude';
|
import { claudChat, claudStreamResponse } from './claude';
|
||||||
@@ -11,6 +11,7 @@ export type ChatCompletionType = {
|
|||||||
apiKey: string;
|
apiKey: string;
|
||||||
temperature: number;
|
temperature: number;
|
||||||
messages: ChatItemSimpleType[];
|
messages: ChatItemSimpleType[];
|
||||||
|
chatId?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
export type ChatCompletionResponseType = {
|
export type ChatCompletionResponseType = {
|
||||||
@@ -23,7 +24,6 @@ export type StreamResponseType = {
|
|||||||
chatResponse: any;
|
chatResponse: any;
|
||||||
prompts: ChatItemSimpleType[];
|
prompts: ChatItemSimpleType[];
|
||||||
res: NextApiResponse;
|
res: NextApiResponse;
|
||||||
systemPrompt?: string;
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
export type StreamResponseReturnType = {
|
export type StreamResponseReturnType = {
|
||||||
@@ -129,7 +129,6 @@ export const resStreamResponse = async ({
|
|||||||
model,
|
model,
|
||||||
res,
|
res,
|
||||||
chatResponse,
|
chatResponse,
|
||||||
systemPrompt,
|
|
||||||
prompts
|
prompts
|
||||||
}: StreamResponseType & {
|
}: StreamResponseType & {
|
||||||
model: ChatModelType;
|
model: ChatModelType;
|
||||||
@@ -139,18 +138,14 @@ export const resStreamResponse = async ({
|
|||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
res.setHeader('X-Accel-Buffering', 'no');
|
res.setHeader('X-Accel-Buffering', 'no');
|
||||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||||
systemPrompt && res.setHeader(SYSTEM_PROMPT_HEADER, encodeURIComponent(systemPrompt));
|
|
||||||
|
|
||||||
const { responseContent, totalTokens, finishMessages } = await modelServiceToolMap[
|
const { responseContent, totalTokens, finishMessages } = await modelServiceToolMap[
|
||||||
model
|
model
|
||||||
].streamResponse({
|
].streamResponse({
|
||||||
chatResponse,
|
chatResponse,
|
||||||
prompts,
|
prompts,
|
||||||
res,
|
res
|
||||||
systemPrompt
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.end();
|
|
||||||
|
|
||||||
return { responseContent, totalTokens, finishMessages };
|
return { responseContent, totalTokens, finishMessages };
|
||||||
};
|
};
|
||||||
|
@@ -1,13 +1,11 @@
|
|||||||
import { Configuration, OpenAIApi } from 'openai';
|
import { Configuration, OpenAIApi } from 'openai';
|
||||||
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||||
import { axiosConfig } from '../tools';
|
import { axiosConfig } from '../tools';
|
||||||
import { ChatModelMap, embeddingModel, OpenAiChatEnum } from '@/constants/model';
|
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
|
||||||
import { pushGenerateVectorBill } from '../../events/pushBill';
|
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||||
import { adaptChatItem_openAI } from '@/utils/chat/openai';
|
import { modelToolMap } from '@/utils/plugin';
|
||||||
import { modelToolMap } from '@/utils/chat';
|
|
||||||
import { ChatCompletionType, ChatContextFilter, StreamResponseType } from './index';
|
import { ChatCompletionType, ChatContextFilter, StreamResponseType } from './index';
|
||||||
import { ChatRoleEnum } from '@/constants/chat';
|
import { ChatRoleEnum } from '@/constants/chat';
|
||||||
import { getSystemOpenAiKey } from '../auth';
|
|
||||||
|
|
||||||
export const getOpenAIApi = () =>
|
export const getOpenAIApi = () =>
|
||||||
new OpenAIApi(
|
new OpenAIApi(
|
||||||
@@ -16,51 +14,6 @@ export const getOpenAIApi = () =>
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
/* 获取向量 */
|
|
||||||
export const openaiCreateEmbedding = async ({
|
|
||||||
userOpenAiKey,
|
|
||||||
userId,
|
|
||||||
textArr
|
|
||||||
}: {
|
|
||||||
userOpenAiKey?: string;
|
|
||||||
userId: string;
|
|
||||||
textArr: string[];
|
|
||||||
}) => {
|
|
||||||
const systemAuthKey = getSystemOpenAiKey();
|
|
||||||
|
|
||||||
// 获取 chatAPI
|
|
||||||
const chatAPI = getOpenAIApi();
|
|
||||||
|
|
||||||
// 把输入的内容转成向量
|
|
||||||
const res = await chatAPI
|
|
||||||
.createEmbedding(
|
|
||||||
{
|
|
||||||
model: embeddingModel,
|
|
||||||
input: textArr
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeout: 60000,
|
|
||||||
...axiosConfig(userOpenAiKey || systemAuthKey)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then((res) => ({
|
|
||||||
tokenLen: res.data.usage.total_tokens || 0,
|
|
||||||
vectors: res.data.data.map((item) => item.embedding)
|
|
||||||
}));
|
|
||||||
|
|
||||||
pushGenerateVectorBill({
|
|
||||||
isPay: !userOpenAiKey,
|
|
||||||
userId,
|
|
||||||
text: textArr.join(''),
|
|
||||||
tokenLen: res.tokenLen
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
vectors: res.vectors,
|
|
||||||
chatAPI
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/* 模型对话 */
|
/* 模型对话 */
|
||||||
export const chatResponse = async ({
|
export const chatResponse = async ({
|
||||||
model,
|
model,
|
||||||
@@ -127,7 +80,7 @@ export const openAiStreamResponse = async ({
|
|||||||
const content: string = json?.choices?.[0].delta.content || '';
|
const content: string = json?.choices?.[0].delta.content || '';
|
||||||
responseContent += content;
|
responseContent += content;
|
||||||
|
|
||||||
res.writable && content && res.write(content);
|
!res.closed && content && res.write(content);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
error;
|
error;
|
||||||
}
|
}
|
||||||
@@ -137,8 +90,7 @@ export const openAiStreamResponse = async ({
|
|||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
const parser = createParser(onParse);
|
const parser = createParser(onParse);
|
||||||
for await (const chunk of chatResponse.data as any) {
|
for await (const chunk of chatResponse.data as any) {
|
||||||
if (!res.writable) {
|
if (res.closed) {
|
||||||
// 流被中断了,直接忽略后面的内容
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
parser.feed(decoder.decode(chunk, { stream: true }));
|
parser.feed(decoder.decode(chunk, { stream: true }));
|
||||||
|
3
src/types/chat.d.ts
vendored
3
src/types/chat.d.ts
vendored
@@ -1,12 +1,13 @@
|
|||||||
import { ChatRoleEnum } from '@/constants/chat';
|
import { ChatRoleEnum } from '@/constants/chat';
|
||||||
import type { InitChatResponse, InitShareChatResponse } from '@/api/response/chat';
|
import type { InitChatResponse, InitShareChatResponse } from '@/api/response/chat';
|
||||||
|
import { QuoteItemType } from '@/pages/api/openapi/kb/appKbSearch';
|
||||||
|
|
||||||
export type ExportChatType = 'md' | 'pdf' | 'html';
|
export type ExportChatType = 'md' | 'pdf' | 'html';
|
||||||
|
|
||||||
export type ChatItemSimpleType = {
|
export type ChatItemSimpleType = {
|
||||||
obj: `${ChatRoleEnum}`;
|
obj: `${ChatRoleEnum}`;
|
||||||
value: string;
|
value: string;
|
||||||
systemPrompt?: string;
|
quote?: QuoteItemType[];
|
||||||
};
|
};
|
||||||
export type ChatItemType = {
|
export type ChatItemType = {
|
||||||
_id: string;
|
_id: string;
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
export const ClaudeSliceTextByToken = ({ text, length }: { text: string; length: number }) => {
|
|
||||||
return text.slice(0, length);
|
|
||||||
};
|
|
@@ -1,6 +1,6 @@
|
|||||||
import mammoth from 'mammoth';
|
import mammoth from 'mammoth';
|
||||||
import Papa from 'papaparse';
|
import Papa from 'papaparse';
|
||||||
import { getOpenAiEncMap } from './chat/openai';
|
import { getOpenAiEncMap } from './plugin/openai';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取 txt 文件内容
|
* 读取 txt 文件内容
|
||||||
|
@@ -2,29 +2,37 @@ import { ClaudeEnum, OpenAiChatEnum } from '@/constants/model';
|
|||||||
import type { ChatModelType } from '@/constants/model';
|
import type { ChatModelType } from '@/constants/model';
|
||||||
import type { ChatItemSimpleType } from '@/types/chat';
|
import type { ChatItemSimpleType } from '@/types/chat';
|
||||||
import { countOpenAIToken, openAiSliceTextByToken } from './openai';
|
import { countOpenAIToken, openAiSliceTextByToken } from './openai';
|
||||||
import { ClaudeSliceTextByToken } from './claude';
|
import { gpt_chatItemTokenSlice } from '@/pages/api/openapi/text/gptMessagesSlice';
|
||||||
|
|
||||||
export const modelToolMap: Record<
|
export const modelToolMap: Record<
|
||||||
ChatModelType,
|
ChatModelType,
|
||||||
{
|
{
|
||||||
countTokens: (data: { messages: ChatItemSimpleType[] }) => number;
|
countTokens: (data: { messages: ChatItemSimpleType[] }) => number;
|
||||||
sliceText: (data: { text: string; length: number }) => string;
|
sliceText: (data: { text: string; length: number }) => string;
|
||||||
|
tokenSlice: (data: {
|
||||||
|
messages: ChatItemSimpleType[];
|
||||||
|
maxToken: number;
|
||||||
|
}) => ChatItemSimpleType[];
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
[OpenAiChatEnum.GPT35]: {
|
[OpenAiChatEnum.GPT35]: {
|
||||||
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }),
|
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }),
|
||||||
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data })
|
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data }),
|
||||||
|
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT35, ...data })
|
||||||
},
|
},
|
||||||
[OpenAiChatEnum.GPT4]: {
|
[OpenAiChatEnum.GPT4]: {
|
||||||
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT4, messages }),
|
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT4, messages }),
|
||||||
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT4, ...data })
|
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT4, ...data }),
|
||||||
|
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT4, ...data })
|
||||||
},
|
},
|
||||||
[OpenAiChatEnum.GPT432k]: {
|
[OpenAiChatEnum.GPT432k]: {
|
||||||
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT432k, messages }),
|
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT432k, messages }),
|
||||||
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT432k, ...data })
|
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT432k, ...data }),
|
||||||
|
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT432k, ...data })
|
||||||
},
|
},
|
||||||
[ClaudeEnum.Claude]: {
|
[ClaudeEnum.Claude]: {
|
||||||
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }),
|
countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }),
|
||||||
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data })
|
sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data }),
|
||||||
|
tokenSlice: (data) => gpt_chatItemTokenSlice({ model: OpenAiChatEnum.GPT35, ...data })
|
||||||
}
|
}
|
||||||
};
|
};
|
@@ -126,3 +126,10 @@ export const getErrText = (err: any, def = '') => {
|
|||||||
msg && console.log('error =>', msg);
|
msg && console.log('error =>', msg);
|
||||||
return msg;
|
return msg;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const delay = (ms: number) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve('');
|
||||||
|
}, ms);
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user