Compare commits

..

8 Commits
v3.9 ... v3.9.2

Author SHA1 Message Date
archer
5be57da407 fix: v1 api 2023-06-24 21:39:34 +08:00
archer
057c3411b9 perf: fetch error 2023-06-24 21:21:53 +08:00
archer
83d755ad0e feat: limit prompt 2023-06-24 18:55:46 +08:00
JustSong
ec9852fc63 docs: update README (#103) 2023-06-24 00:38:08 +08:00
archer
4e6f8aefe8 docs 2023-06-23 23:40:07 +08:00
archer
11352b754a fix: model 2023-06-23 23:21:59 +08:00
archer
965ad34283 docs 2023-06-23 23:16:49 +08:00
archer
986206b691 perf: sse response 2023-06-23 23:11:22 +08:00
28 changed files with 402 additions and 286 deletions

View File

@@ -44,9 +44,10 @@ Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接
## Powered by ## Powered by
- [TuShan 5 分钟搭建后台管理系统](https://github.com/msgbyte/tushan) - [TuShan: 5 分钟搭建后台管理系统](https://github.com/msgbyte/tushan)
- [Laf 3 分钟快速接入三方应用](https://github.com/labring/laf) - [Laf: 3 分钟快速接入三方应用](https://github.com/labring/laf)
- [Sealos 快速部署集群应用](https://github.com/labring/sealos) - [Sealos: 快速部署集群应用](https://github.com/labring/sealos)
- [One API: 令牌管理 & 二次分发,支持 Azure](https://github.com/songquanpeng/one-api)
## 🌟 Star History ## 🌟 Star History

View File

@@ -17,10 +17,12 @@ aliTemplateCode=xxxx
TOKEN_KEY=dfdasfdas TOKEN_KEY=dfdasfdas
# root key, 最高权限 # root key, 最高权限
ROOT_KEY=fdafasd ROOT_KEY=fdafasd
# openai # 使用 oneapi
# OPENAI_BASE_URL=http://ai.openai.com/v1 # ONEAPI_URL=https://xxxx.cloud.sealos.io/v1
# OPENAI_BASE_URL_AUTH=可选安全凭证,会放到 header.auth 里 # ONEAPI_KEY=sk-xxxx
OPENAIKEY=sk-xxx # openai 的基本地址(国外的可以忽略,默认走 api.openai.com。不用 oneapi 的话需要下面 2 个参数,用户的 key 也会走下面的参数
OPENAI_BASE_URL=https://xxxx.cloud.sealos.io/openai/v1
OPENAIKEY=sk-xxxx
# db # db
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin
MONGODB_NAME=fastgpt MONGODB_NAME=fastgpt

View File

@@ -26,7 +26,6 @@
"crypto": "^1.0.1", "crypto": "^1.0.1",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"eventsource-parser": "^0.1.0",
"formidable": "^2.1.1", "formidable": "^2.1.1",
"framer-motion": "^9.0.6", "framer-motion": "^9.0.6",
"hyperdown": "^2.4.29", "hyperdown": "^2.4.29",

10
client/pnpm-lock.yaml generated
View File

@@ -56,9 +56,6 @@ dependencies:
dayjs: dayjs:
specifier: ^1.11.7 specifier: ^1.11.7
version: registry.npmmirror.com/dayjs@1.11.7 version: registry.npmmirror.com/dayjs@1.11.7
eventsource-parser:
specifier: ^0.1.0
version: registry.npmmirror.com/eventsource-parser@0.1.0
formidable: formidable:
specifier: ^2.1.1 specifier: ^2.1.1
version: registry.npmmirror.com/formidable@2.1.1 version: registry.npmmirror.com/formidable@2.1.1
@@ -7510,13 +7507,6 @@ packages:
version: 2.0.3 version: 2.0.3
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
registry.npmmirror.com/eventsource-parser@0.1.0:
resolution: {integrity: sha512-M9QjFtEIkwytUarnx113HGmgtk52LSn3jNAtnWKi3V+b9rqSfQeVdLsaD5AG/O4IrGQwmAAHBIsqbmURPTd2rA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-0.1.0.tgz}
name: eventsource-parser
version: 0.1.0
engines: {node: '>=14.18'}
dev: false
registry.npmmirror.com/execa@5.1.1: registry.npmmirror.com/execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz} resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz}
name: execa name: execa

View File

@@ -1,6 +1,7 @@
import { Props, ChatResponseType } from '@/pages/api/openapi/v1/chat/completions'; import { Props, ChatResponseType } from '@/pages/api/openapi/v1/chat/completions';
import { sseResponseEventEnum } from '@/constants/chat'; import { sseResponseEventEnum } from '@/constants/chat';
import { getErrText } from '@/utils/tools'; import { getErrText } from '@/utils/tools';
import { parseStreamChunk } from '@/utils/adapt';
interface StreamFetchProps { interface StreamFetchProps {
data: Props; data: Props;
@@ -8,103 +9,95 @@ interface StreamFetchProps {
abortSignal: AbortController; abortSignal: AbortController;
} }
export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) => export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) =>
new Promise<ChatResponseType & { responseText: string }>(async (resolve, reject) => { new Promise<ChatResponseType & { responseText: string; errMsg: string }>(
try { async (resolve, reject) => {
const response = await window.fetch('/api/openapi/v1/chat/completions', { try {
method: 'POST', const response = await window.fetch('/api/openapi/v1/chat/completions', {
headers: { method: 'POST',
'Content-Type': 'application/json' headers: {
}, 'Content-Type': 'application/json'
signal: abortSignal.signal, },
body: JSON.stringify({ signal: abortSignal.signal,
...data, body: JSON.stringify({
stream: true ...data,
}) stream: true
}); })
});
if (response.status !== 200) { if (response.status !== 200) {
const err = await response.json(); const err = await response.json();
return reject(err); return reject(err);
} }
if (!response?.body) { if (!response?.body) {
throw new Error('Request Error'); throw new Error('Request Error');
} }
const reader = response.body?.getReader(); const reader = response.body?.getReader();
const decoder = new TextDecoder('utf-8');
// response data // response data
let responseText = ''; let responseText = '';
let newChatId = ''; let newChatId = '';
let quoteLen = 0; let quoteLen = 0;
let errMsg = '';
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 (response.status === 200) { if (response.status === 200) {
return resolve({
responseText,
newChatId,
quoteLen,
errMsg
});
} else {
return reject('响应过程出现异常~');
}
}
const chunkResponse = parseStreamChunk(value);
chunkResponse.forEach((item) => {
// parse json data
const data = (() => {
try {
return JSON.parse(item.data);
} catch (error) {
return item.data;
}
})();
if (item.event === sseResponseEventEnum.answer && data !== '[DONE]') {
const answer: string = data?.choices?.[0].delta.content || '';
onMessage(answer);
responseText += answer;
} else if (item.event === sseResponseEventEnum.chatResponse) {
const chatResponse = data as ChatResponseType;
newChatId = chatResponse.newChatId;
quoteLen = chatResponse.quoteLen || 0;
} else if (item.event === sseResponseEventEnum.error) {
errMsg = getErrText(data, '流响应错误');
}
});
read();
} catch (err: any) {
if (err?.message === 'The user aborted a request.') {
return resolve({ return resolve({
responseText, responseText,
newChatId, newChatId,
quoteLen quoteLen,
errMsg
}); });
} else {
return reject('响应过程出现异常~');
} }
reject(getErrText(err, '请求异常'));
} }
const chunk = decoder.decode(value); };
const chunkLines = chunk.split('\n\n').filter((item) => item); read();
const chunkResponse = chunkLines.map((item) => { } catch (err: any) {
const splitEvent = item.split('\n'); console.log(err);
if (splitEvent.length === 2) {
return {
event: splitEvent[0].replace('event: ', ''),
data: splitEvent[1].replace('data: ', '')
};
}
return {
event: '',
data: splitEvent[0].replace('data: ', '')
};
});
chunkResponse.forEach((item) => { reject(getErrText(err, '请求异常'));
// parse json data }
const data = (() => {
try {
return JSON.parse(item.data);
} catch (error) {
return item.data;
}
})();
if (item.event === sseResponseEventEnum.answer && data !== '[DONE]') {
const answer: string = data?.choices?.[0].delta.content || '';
onMessage(answer);
responseText += answer;
} else if (item.event === sseResponseEventEnum.chatResponse) {
const chatResponse = data as ChatResponseType;
newChatId = chatResponse.newChatId;
quoteLen = chatResponse.quoteLen || 0;
}
});
read();
} catch (err: any) {
if (err?.message === 'The user aborted a request.') {
return resolve({
responseText,
newChatId,
quoteLen
});
}
reject(getErrText(err, '请求异常'));
}
};
read();
} catch (err: any) {
console.log(err);
reject(getErrText(err, '请求异常'));
} }
}); );

View File

@@ -5,6 +5,7 @@ export interface InitChatResponse {
chatId: string; chatId: string;
modelId: string; modelId: string;
systemPrompt?: string; systemPrompt?: string;
limitPrompt?: string;
model: { model: {
name: string; name: string;
avatar: string; avatar: string;

View File

@@ -26,7 +26,7 @@ const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props
return ( return (
<Menu autoSelect={false} onOpen={onOpen} onClose={onClose}> <Menu autoSelect={false} onOpen={onOpen} onClose={onClose}>
<MenuButton as={'span'}> <MenuButton style={{ width: '100%' }} as={'span'}>
<Button <Button
width={width} width={width}
px={3} px={3}

View File

@@ -1,4 +1,5 @@
export enum sseResponseEventEnum { export enum sseResponseEventEnum {
error = 'error',
answer = 'answer', answer = 'answer',
chatResponse = 'chatResponse' chatResponse = 'chatResponse'
} }

View File

@@ -82,6 +82,7 @@ export const defaultModel: ModelSchema = {
searchLimit: 5, searchLimit: 5,
searchEmptyText: '', searchEmptyText: '',
systemPrompt: '', systemPrompt: '',
limitPrompt: '',
temperature: 0, temperature: 0,
maxToken: 4000, maxToken: 4000,
chatModel: OpenAiChatEnum.GPT35 chatModel: OpenAiChatEnum.GPT35

View File

@@ -101,6 +101,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, },
chatModel: model.chat.chatModel, chatModel: model.chat.chatModel,
systemPrompt: isOwner ? model.chat.systemPrompt : '', systemPrompt: isOwner ? model.chat.systemPrompt : '',
limitPrompt: isOwner ? model.chat.limitPrompt : '',
history history
} }
}); });

View File

@@ -65,10 +65,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
const modelConstantsData = ChatModelMap[model.chat.chatModel]; const modelConstantsData = ChatModelMap[model.chat.chatModel];
const prompt = prompts[prompts.length - 1]; const prompt = prompts[prompts.length - 1];
const { userSystemPrompt = [], quotePrompt = [] } = await (async () => { const {
userSystemPrompt = [],
userLimitPrompt = [],
quotePrompt = []
} = await (async () => {
// 使用了知识库搜索 // 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) { if (model.chat.relatedKbs?.length > 0) {
const { userSystemPrompt, quotePrompt } = await appKbSearch({ const { quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
model, model,
userId, userId,
fixedQuote: [], fixedQuote: [],
@@ -78,21 +82,29 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}); });
return { return {
userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [], userSystemPrompt,
userLimitPrompt,
quotePrompt: [quotePrompt] quotePrompt: [quotePrompt]
}; };
} }
if (model.chat.systemPrompt) { return {
return { userSystemPrompt: model.chat.systemPrompt
userSystemPrompt: [ ? [
{ {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.System,
value: model.chat.systemPrompt value: model.chat.systemPrompt
} }
] ]
}; : [],
} userLimitPrompt: model.chat.limitPrompt
return {}; ? [
{
obj: ChatRoleEnum.Human,
value: model.chat.limitPrompt
}
]
: []
};
})(); })();
// search result is empty // search result is empty
@@ -102,7 +114,13 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
} }
// 读取对话内容 // 读取对话内容
const completePrompts = [...quotePrompt, ...prompts.slice(0, -1), ...userSystemPrompt, prompt]; const completePrompts = [
...quotePrompt,
...userSystemPrompt,
...prompts.slice(0, -1),
...userLimitPrompt,
prompt
];
// 计算温度 // 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed( const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(

View File

@@ -28,7 +28,11 @@ type Response = {
userSystemPrompt: { userSystemPrompt: {
obj: ChatRoleEnum; obj: ChatRoleEnum;
value: string; value: string;
}; }[];
userLimitPrompt: {
obj: ChatRoleEnum;
value: string;
}[];
quotePrompt: { quotePrompt: {
obj: ChatRoleEnum; obj: ChatRoleEnum;
value: string; value: string;
@@ -130,17 +134,24 @@ export async function appKbSearch({
// 计算固定提示词的 token 数量 // 计算固定提示词的 token 数量
const userSystemPrompt = model.chat.systemPrompt // user system prompt const userSystemPrompt = model.chat.systemPrompt // user system prompt
? { ? [
obj: ChatRoleEnum.Human, {
value: model.chat.systemPrompt obj: ChatRoleEnum.System,
} value: model.chat.systemPrompt
: { }
obj: ChatRoleEnum.Human, ]
value: `知识库是关于 ${model.name} 的内容,参考知识库回答问题。与 "${model.name}" 无关内容,直接回复: "我不知道"。` : [];
}; const userLimitPrompt = [
{
obj: ChatRoleEnum.Human,
value: model.chat.limitPrompt
? model.chat.limitPrompt
: `知识库是关于 ${model.name} 的内容,参考知识库回答问题。与 "${model.name}" 无关内容,直接回复: "我不知道"。`
}
];
const fixedSystemTokens = modelToolMap[model.chat.chatModel].countTokens({ const fixedSystemTokens = modelToolMap[model.chat.chatModel].countTokens({
messages: [userSystemPrompt] messages: [...userSystemPrompt, ...userLimitPrompt]
}); });
// filter part quote by maxToken // filter part quote by maxToken
@@ -164,6 +175,7 @@ export async function appKbSearch({
return { return {
rawSearch, rawSearch,
userSystemPrompt, userSystemPrompt,
userLimitPrompt,
quotePrompt: { quotePrompt: {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.System,
value: quoteText value: quoteText

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { authUser, getApiKey } from '@/service/utils/auth'; import { authUser, getApiKey, getSystemOpenAiKey } from '@/service/utils/auth';
import { withNextCors } from '@/service/utils/tools'; import { withNextCors } from '@/service/utils/tools';
import { getOpenAIApi } from '@/service/utils/chat/openai'; import { getOpenAIApi } from '@/service/utils/chat/openai';
import { embeddingModel } from '@/constants/model'; import { embeddingModel } from '@/constants/model';
@@ -39,14 +39,10 @@ export async function openaiEmbedding({
input, input,
mustPay = false mustPay = false
}: { userId: string; mustPay?: boolean } & Props) { }: { userId: string; mustPay?: boolean } & Props) {
const { userOpenAiKey, systemAuthKey } = await getApiKey({ const apiKey = getSystemOpenAiKey();
model: OpenAiChatEnum.GPT35,
userId,
mustPay
});
// 获取 chatAPI // 获取 chatAPI
const chatAPI = getOpenAIApi(); const chatAPI = getOpenAIApi(apiKey);
// 把输入的内容转成向量 // 把输入的内容转成向量
const result = await chatAPI const result = await chatAPI
@@ -57,16 +53,22 @@ export async function openaiEmbedding({
}, },
{ {
timeout: 60000, timeout: 60000,
...axiosConfig(userOpenAiKey || systemAuthKey) ...axiosConfig(apiKey)
} }
) )
.then((res) => ({ .then((res) => {
tokenLen: res.data?.usage?.total_tokens || 0, if (!res.data?.usage?.total_tokens) {
vectors: res.data.data.map((item) => item.embedding) // @ts-ignore
})); return Promise.reject(res.data?.error?.message || 'Embedding Error');
}
return {
tokenLen: res.data.usage.total_tokens || 0,
vectors: res.data.data.map((item) => item.embedding)
};
});
pushGenerateVectorBill({ pushGenerateVectorBill({
isPay: !userOpenAiKey, isPay: mustPay,
userId, userId,
text: input.join(''), text: input.join(''),
tokenLen: result.tokenLen tokenLen: result.tokenLen

View File

@@ -14,9 +14,9 @@ import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt';
import { getChatHistory } from './getHistory'; import { getChatHistory } from './getHistory';
import { saveChat } from '@/pages/api/chat/saveChat'; import { saveChat } from '@/pages/api/chat/saveChat';
import { sseResponse } from '@/service/utils/tools'; import { sseResponse } from '@/service/utils/tools';
import { getErrText } from '@/utils/tools';
import { type ChatCompletionRequestMessage } from 'openai'; import { type ChatCompletionRequestMessage } from 'openai';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { sensitiveCheck } from '../../text/sensitiveCheck';
export type MessageItemType = ChatCompletionRequestMessage & { _id?: string }; export type MessageItemType = ChatCompletionRequestMessage & { _id?: string };
type FastGptWebChatProps = { type FastGptWebChatProps = {
@@ -108,11 +108,12 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
const { const {
rawSearch = [], rawSearch = [],
userSystemPrompt = [], userSystemPrompt = [],
userLimitPrompt = [],
quotePrompt = [] quotePrompt = []
} = await (async () => { } = await (async () => {
// 使用了知识库搜索 // 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) { if (model.chat.relatedKbs?.length > 0) {
const { rawSearch, userSystemPrompt, quotePrompt } = await appKbSearch({ const { rawSearch, quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
model, model,
userId, userId,
fixedQuote: history[history.length - 1]?.quote, fixedQuote: history[history.length - 1]?.quote,
@@ -123,21 +124,29 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
return { return {
rawSearch, rawSearch,
userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [], userSystemPrompt,
userLimitPrompt,
quotePrompt: [quotePrompt] quotePrompt: [quotePrompt]
}; };
} }
if (model.chat.systemPrompt) { return {
return { userSystemPrompt: model.chat.systemPrompt
userSystemPrompt: [ ? [
{ {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.System,
value: model.chat.systemPrompt value: model.chat.systemPrompt
} }
] ]
}; : [],
} userLimitPrompt: model.chat.limitPrompt
return {}; ? [
{
obj: ChatRoleEnum.Human,
value: model.chat.limitPrompt
}
]
: []
};
})(); })();
// search result is empty // search result is empty
@@ -167,7 +176,13 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
} }
// api messages. [quote,context,systemPrompt,question] // api messages. [quote,context,systemPrompt,question]
const completePrompts = [...quotePrompt, ...prompts.slice(0, -1), ...userSystemPrompt, prompt]; const completePrompts = [
...quotePrompt,
...userSystemPrompt,
...prompts.slice(0, -1),
...userLimitPrompt,
prompt
];
// chat temperature // chat temperature
const modelConstantsData = ChatModelMap[model.chat.chatModel]; const modelConstantsData = ChatModelMap[model.chat.chatModel];
// FastGpt temperature range: 1~10 // FastGpt temperature range: 1~10
@@ -175,6 +190,10 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
2 2
); );
await sensitiveCheck({
input: `${userSystemPrompt[0]?.value}\n${userLimitPrompt[0]?.value}\n${prompt.value}`
});
// start model api. responseText and totalTokens: valid only if stream = false // start model api. responseText and totalTokens: valid only if stream = false
const { streamResponse, responseMessages, responseText, totalTokens } = const { streamResponse, responseMessages, responseText, totalTokens } =
await modelServiceToolMap[model.chat.chatModel].chatCompletion({ await modelServiceToolMap[model.chat.chatModel].chatCompletion({
@@ -231,8 +250,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
tokens: totalTokens tokens: totalTokens
}; };
} catch (error) { } catch (error) {
console.log('stream response error', error); return Promise.reject(error);
return {};
} }
} else { } else {
return { return {
@@ -256,7 +274,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
...(showAppDetail ...(showAppDetail
? { ? {
quote: rawSearch, quote: rawSearch,
systemPrompt: userSystemPrompt?.[0]?.value systemPrompt: `${userSystemPrompt[0]?.value}\n\n${userLimitPrompt[0]?.value}`
} }
: {}) : {})
} }
@@ -301,7 +319,12 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
} catch (err: any) { } catch (err: any) {
res.status(500); res.status(500);
if (step === 1) { if (step === 1) {
res.end(getErrText(err, 'Stream response error')); sseResponse({
res,
event: sseResponseEventEnum.error,
data: JSON.stringify(err)
});
res.end();
} else { } else {
jsonRes(res, { jsonRes(res, {
code: 500, code: 500,

View File

@@ -7,11 +7,9 @@ import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const chatModelList: ChatModelItemType[] = []; const chatModelList: ChatModelItemType[] = [];
if (process.env.OPENAIKEY) { chatModelList.push(ChatModelMap[OpenAiChatEnum.GPT3516k]);
chatModelList.push(ChatModelMap[OpenAiChatEnum.GPT3516k]); chatModelList.push(ChatModelMap[OpenAiChatEnum.GPT35]);
chatModelList.push(ChatModelMap[OpenAiChatEnum.GPT35]); chatModelList.push(ChatModelMap[OpenAiChatEnum.GPT4]);
chatModelList.push(ChatModelMap[OpenAiChatEnum.GPT4]);
}
jsonRes(res, { jsonRes(res, {
data: chatModelList data: chatModelList

View File

@@ -174,7 +174,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
const messages = adaptChatItem_openAI({ messages: prompts, reserveId: true }); const messages = adaptChatItem_openAI({ messages: prompts, reserveId: true });
// 流请求,获取数据 // 流请求,获取数据
const { newChatId, quoteLen } = await streamFetch({ const { newChatId, quoteLen, errMsg } = await streamFetch({
data: { data: {
messages, messages,
chatId, chatId,
@@ -219,7 +219,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
...item, ...item,
status: 'finish', status: 'finish',
quoteLen, quoteLen,
systemPrompt: chatData.systemPrompt systemPrompt: `${chatData.systemPrompt}${`${
chatData.limitPrompt ? `\n\n${chatData.limitPrompt}` : ''
}`}`
}; };
}) })
})); }));
@@ -230,17 +232,26 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
loadHistory({ pageNum: 1, init: true }); loadHistory({ pageNum: 1, init: true });
loadMyModels(true); loadMyModels(true);
}, 100); }, 100);
if (errMsg) {
toast({
status: 'warning',
title: errMsg
});
}
}, },
[ [
chatId, chatId,
modelId, modelId,
chatData.systemPrompt,
setChatData, setChatData,
loadHistory,
loadMyModels,
generatingMessage, generatingMessage,
setForbidLoadChatData, setForbidLoadChatData,
router router,
chatData.systemPrompt,
chatData.limitPrompt,
loadHistory,
loadMyModels,
toast
] ]
); );
@@ -749,7 +760,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
px={[2, 4]} px={[2, 4]}
onClick={() => setShowSystemPrompt(item.systemPrompt || '')} onClick={() => setShowSystemPrompt(item.systemPrompt || '')}
> >
&
</Button> </Button>
)} )}
{!!item.quoteLen && ( {!!item.quoteLen && (

View File

@@ -42,6 +42,12 @@ const Test = () => {
pushKbTestItem(testItem); pushKbTestItem(testItem);
setInputText(''); setInputText('');
setKbTestItem(testItem); setKbTestItem(testItem);
},
onError(err) {
toast({
title: getErrText(err),
status: 'error'
});
} }
}); });

View File

@@ -1,5 +1,15 @@
import React, { useCallback, useState, useMemo } from 'react'; import React, { useCallback, useState, useMemo } from 'react';
import { Box, Flex, Button, FormControl, Input, Textarea, Divider } from '@chakra-ui/react'; import {
Box,
Flex,
Button,
FormControl,
Input,
Textarea,
Divider,
Tooltip
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -20,6 +30,11 @@ import Avatar from '@/components/Avatar';
import MySelect from '@/components/Select'; import MySelect from '@/components/Select';
import MySlider from '@/components/Slider'; import MySlider from '@/components/Slider';
const systemPromptTip =
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。';
const limitPromptTip =
'限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"';
const Settings = ({ modelId }: { modelId: string }) => { const Settings = ({ modelId }: { modelId: string }) => {
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter(); const router = useRouter();
@@ -60,7 +75,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
} }
return max; return max;
}, [getValues, setValue, refresh]); }, [getValues, setValue]);
// 提交保存模型修改 // 提交保存模型修改
const saveSubmitSuccess = useCallback( const saveSubmitSuccess = useCallback(
@@ -211,7 +226,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
</Box> </Box>
<Textarea <Textarea
rows={5} rows={4}
maxLength={500} maxLength={500}
placeholder={'给你的 AI 应用一个介绍'} placeholder={'给你的 AI 应用一个介绍'}
{...register('intro')} {...register('intro')}
@@ -225,11 +240,14 @@ const Settings = ({ modelId }: { modelId: string }) => {
</Box> </Box>
<MySelect <MySelect
width={['200px', '240px']} width={['100%', '280px']}
value={getValues('chat.chatModel')} value={getValues('chat.chatModel')}
list={chatModelList.map((item) => ({ list={chatModelList.map((item) => ({
id: item.chatModel, id: item.chatModel,
label: item.name label: `${item.name} (${formatPrice(
ChatModelMap[getValues('chat.chatModel')]?.price,
1000
)} 元/1k tokens)`
}))} }))}
onchange={(val: any) => { onchange={(val: any) => {
setValue('chat.chatModel', val); setValue('chat.chatModel', val);
@@ -237,15 +255,6 @@ const Settings = ({ modelId }: { modelId: string }) => {
}} }}
/> />
</Flex> </Flex>
<Flex alignItems={'center'} mt={5}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Box fontSize={['sm', 'md']}>
{formatPrice(ChatModelMap[getValues('chat.chatModel')]?.price, 1000)}
/1K tokens()
</Box>
</Flex>
<Flex alignItems={'center'} my={10}> <Flex alignItems={'center'} my={10}>
<Box w={['60px', '100px', '140px']} flexShrink={0}> <Box w={['60px', '100px', '140px']} flexShrink={0}>
@@ -269,7 +278,7 @@ const Settings = ({ modelId }: { modelId: string }) => {
</Flex> </Flex>
<Flex alignItems={'center'} mt={12} mb={10}> <Flex alignItems={'center'} mt={12} mb={10}>
<Box w={['60px', '100px', '140px']} flexShrink={0}> <Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box> </Box>
<Box flex={1} ml={'10px'}> <Box flex={1} ml={'10px'}>
<MySlider <MySlider
@@ -292,15 +301,29 @@ const Settings = ({ modelId }: { modelId: string }) => {
<Flex mt={10} alignItems={'flex-start'}> <Flex mt={10} alignItems={'flex-start'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}> <Box w={['60px', '100px', '140px']} flexShrink={0}>
<Tooltip label={systemPromptTip}>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</Tooltip>
</Box> </Box>
<Textarea <Textarea
rows={8} rows={8}
placeholder={ placeholder={systemPromptTip}
'模型默认的 prompt 词,通过调整该内容,可以引导模型聊天方向。\n\n如果使用了知识库搜索没有填写该内容时系统会自动补充提示词如果填写了内容则以填写的内容为准。'
}
{...register('chat.systemPrompt')} {...register('chat.systemPrompt')}
></Textarea> ></Textarea>
</Flex> </Flex>
<Flex mt={5} alignItems={'flex-start'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
<Tooltip label={limitPromptTip}>
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
</Tooltip>
</Box>
<Textarea
rows={5}
placeholder={limitPromptTip}
{...register('chat.limitPrompt')}
></Textarea>
</Flex>
<Flex mt={5} alignItems={'center'}> <Flex mt={5} alignItems={'center'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}></Box> <Box w={['60px', '100px', '140px']} flexShrink={0}></Box>

View File

@@ -43,7 +43,10 @@ const ModelSchema = new Schema({
default: '' default: ''
}, },
systemPrompt: { systemPrompt: {
// 系统提示词 type: String,
default: ''
},
limitPrompt: {
type: String, type: String,
default: '' default: ''
}, },

View File

@@ -163,7 +163,7 @@ export const authUser = async ({
/* random get openai api key */ /* random get openai api key */
export const getSystemOpenAiKey = () => { export const getSystemOpenAiKey = () => {
return process.env.OPENAIKEY || ''; return process.env.ONEAPI_KEY || process.env.OPENAIKEY || '';
}; };
/* 获取 api 请求的 key */ /* 获取 api 请求的 key */

View File

@@ -6,8 +6,8 @@ import { sseResponse } from '../tools';
import { OpenAiChatEnum } from '@/constants/model'; import { OpenAiChatEnum } from '@/constants/model';
import { chatResponse, openAiStreamResponse } from './openai'; import { chatResponse, openAiStreamResponse } from './openai';
import type { NextApiResponse } from 'next'; import type { NextApiResponse } from 'next';
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
import { textAdaptGptResponse } from '@/utils/adapt'; import { textAdaptGptResponse } from '@/utils/adapt';
import { parseStreamChunk } from '@/utils/adapt';
export type ChatCompletionType = { export type ChatCompletionType = {
apiKey: string; apiKey: string;
@@ -185,65 +185,63 @@ export const V2_StreamResponse = async ({
model: ChatModelType; model: ChatModelType;
}) => { }) => {
let responseContent = ''; let responseContent = '';
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;
responseContent += 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 { try {
const onParse = async (e: ParsedEvent | ReconnectInterval) => { for await (const chunk of chatResponse.data as any) {
if (e.type !== 'event') return; if (res.closed) break;
const parse = parseStreamChunk(chunk);
const data = e.data; parse.forEach((item) => clientRes(item.data));
const { content = '' } = (() => {
try {
const json = JSON.parse(data);
const content: string = json?.choices?.[0].delta.content || '';
responseContent += content;
return { content };
} catch (error) {}
return {};
})();
if (res.closed) 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 {
const parser = createParser(onParse);
const decoder = new TextDecoder();
for await (const chunk of chatResponse.data as any) {
if (res.closed) {
break;
}
parser.feed(decoder.decode(chunk, { stream: true }));
}
} catch (error) {
console.log('pipe error', error);
} }
} catch (error) { } catch (error) {
console.log('stream error', error); console.log('pipe error', error);
} }
if (error) {
console.log(error);
return Promise.reject(error);
}
// count tokens // count tokens
const finishMessages = prompts.concat({ const finishMessages = prompts.concat({
obj: ChatRoleEnum.AI, obj: ChatRoleEnum.AI,

View File

@@ -1,18 +1,20 @@
import { Configuration, OpenAIApi } from 'openai'; import { Configuration, OpenAIApi } from 'openai';
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
import { axiosConfig } from '../tools'; import { axiosConfig } from '../tools';
import { ChatModelMap, OpenAiChatEnum } from '@/constants/model'; import { ChatModelMap, OpenAiChatEnum } from '@/constants/model';
import { adaptChatItem_openAI } from '@/utils/plugin/openai'; import { adaptChatItem_openAI } from '@/utils/plugin/openai';
import { modelToolMap } from '@/utils/plugin'; import { modelToolMap } from '@/utils/plugin';
import { ChatCompletionType, ChatContextFilter, StreamResponseType } from './index'; import { ChatCompletionType, ChatContextFilter, StreamResponseType } from './index';
import { ChatRoleEnum } from '@/constants/chat'; import { ChatRoleEnum } from '@/constants/chat';
import { parseStreamChunk } from '@/utils/adapt';
export const getOpenAIApi = () => export const getOpenAIApi = (apiKey: string) => {
new OpenAIApi( const openaiBaseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
return new OpenAIApi(
new Configuration({ new Configuration({
basePath: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1' basePath: apiKey === process.env.ONEAPI_KEY ? process.env.ONEAPI_URL : openaiBaseUrl
}) })
); );
};
/* 模型对话 */ /* 模型对话 */
export const chatResponse = async ({ export const chatResponse = async ({
@@ -31,7 +33,7 @@ export const chatResponse = async ({
}); });
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false }); const adaptMessages = adaptChatItem_openAI({ messages: filterMessages, reserveId: false });
const chatAPI = getOpenAIApi(); const chatAPI = getOpenAIApi(apiKey);
const promptsToken = modelToolMap[model].countTokens({ const promptsToken = modelToolMap[model].countTokens({
messages: filterMessages messages: filterMessages
@@ -47,8 +49,8 @@ export const chatResponse = async ({
messages: adaptMessages, messages: adaptMessages,
frequency_penalty: 0.5, // 越大,重复内容越少 frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容 presence_penalty: -0.5, // 越大,越容易出现新内容
stream, stream
stop: ['.!?。'] // stop: ['.!?。']
}, },
{ {
timeout: stream ? 60000 : 480000, timeout: stream ? 60000 : 480000,
@@ -80,29 +82,29 @@ export const openAiStreamResponse = async ({
try { try {
let responseContent = ''; let responseContent = '';
const onParse = async (event: ParsedEvent | ReconnectInterval) => { const clientRes = async (data: string) => {
if (event.type !== 'event') return; const { content = '' } = (() => {
const data = event.data; try {
if (data === '[DONE]') return; const json = JSON.parse(data);
try { const content: string = json?.choices?.[0].delta.content || '';
const json = JSON.parse(data); responseContent += content;
const content: string = json?.choices?.[0].delta.content || ''; return { content };
responseContent += content; } catch (error) {
return {};
}
})();
!res.closed && content && res.write(content); if (data === '[DONE]') return;
} catch (error) {
error; !res.closed && content && res.write(content);
}
}; };
try { try {
const decoder = new TextDecoder();
const parser = createParser(onParse);
for await (const chunk of chatResponse.data as any) { for await (const chunk of chatResponse.data as any) {
if (res.closed) { if (res.closed) break;
break;
} const parse = parseStreamChunk(chunk);
parser.feed(decoder.decode(chunk, { stream: true })); parse.forEach((item) => clientRes(item.data));
} }
} catch (error) { } catch (error) {
console.log('pipe error', error); console.log('pipe error', error);

View File

@@ -34,14 +34,18 @@ export const clearCookie = (res: NextApiResponse) => {
}; };
/* openai axios config */ /* openai axios config */
export const axiosConfig = (apikey: string) => ({ export const axiosConfig = (apikey: string) => {
baseURL: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1', const openaiBaseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
httpsAgent: global.httpsAgent,
headers: { return {
Authorization: `Bearer ${apikey}`, baseURL: apikey === process.env.ONEAPI_KEY ? process.env.ONEAPI_URL : openaiBaseUrl, // 此处仅对非 npm 模块有效
auth: process.env.OPENAI_BASE_URL_AUTH || '' httpsAgent: global.httpsAgent,
} headers: {
}); Authorization: `Bearer ${apikey}`,
auth: process.env.OPENAI_BASE_URL_AUTH || ''
}
};
};
export function withNextCors(handler: NextApiHandler): NextApiHandler { export function withNextCors(handler: NextApiHandler): NextApiHandler {
return async function nextApiHandlerWrappedWithNextCors( return async function nextApiHandlerWrappedWithNextCors(

View File

@@ -43,6 +43,7 @@ export interface ModelSchema {
searchLimit: number; searchLimit: number;
searchEmptyText: string; searchEmptyText: string;
systemPrompt: string; systemPrompt: string;
limitPrompt: string;
temperature: number; temperature: number;
maxToken: number; maxToken: number;
chatModel: ChatModelType; // 聊天时用的模型,训练后就是训练的模型 chatModel: ChatModelType; // 聊天时用的模型,训练后就是训练的模型

View File

@@ -54,3 +54,24 @@ export const textAdaptGptResponse = ({
choices: [{ delta: text === null ? {} : { content: text }, index: 0, finish_reason }] choices: [{ delta: text === null ? {} : { content: text }, index: 0, finish_reason }]
}); });
}; };
const decoder = new TextDecoder();
export const parseStreamChunk = (value: BufferSource) => {
const chunk = decoder.decode(value);
const chunkLines = chunk.split('\n\n').filter((item) => item);
const chunkResponse = chunkLines.map((item) => {
const splitEvent = item.split('\n');
if (splitEvent.length === 2) {
return {
event: splitEvent[0].replace('event: ', ''),
data: splitEvent[1].replace('data: ', '')
};
}
return {
event: '',
data: splitEvent[0].replace('data: ', '')
};
});
return chunkResponse;
};

View File

@@ -116,7 +116,7 @@ export const voiceBroadcast = ({ text }: { text: string }) => {
}; };
export const getErrText = (err: any, def = '') => { export const getErrText = (err: any, def = '') => {
const msg = typeof err === 'string' ? err : err?.message || def || ''; const msg: string = typeof err === 'string' ? err : err?.message || def || '';
msg && console.log('error =>', msg); msg && console.log('error =>', msg);
return msg; return msg;
}; };

View File

@@ -67,10 +67,13 @@ services:
- PG_USER=fastgpt - PG_USER=fastgpt
- PG_PASSWORD=1234 - PG_PASSWORD=1234
- PG_DB_NAME=fastgpt - PG_DB_NAME=fastgpt
# openai, 推荐使用 one-api 管理key # oneapi 配置 推荐使用 one-api 管理key
- ONEAPI_URL=https://kfcwurtbijvh.cloud.sealos.io/v1
- ONEAPI_KEY=sk-itJ9v8qthRiFDzfs62Ea21Aa9b004c8791937dCf4cC568Ff
# openai 相关配置:使用了 oneapi 后,下面只需要填下 OPENAI_BASE_URL (国外可全忽略)
- OPENAIKEY=sk-xxxxx - OPENAIKEY=sk-xxxxx
- OPENAI_BASE_URL=https://api.openai.com/v1 - OPENAI_BASE_URL=https://api.openai.com/v1
- OPENAI_BASE_URL_AUTH=可选的安全凭证 - OPENAI_BASE_URL_AUTH=可选的安全凭证,会放到 header.auth 里
fastgpt-admin: fastgpt-admin:
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-admin:latest image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-admin:latest
container_name: fastgpt-admin container_name: fastgpt-admin

View File

@@ -12,6 +12,8 @@ https://cloud.sealos.io/
## 4. 填写对应参数 ## 4. 填写对应参数
镜像ghcr.io/songquanpeng/one-api:latest
![step2](./sealosImg/step2.png) ![step2](./sealosImg/step2.png)
打开外网访问开关后Sealos 会自动分配一个可访问的地址,不需要自己配置。 打开外网访问开关后Sealos 会自动分配一个可访问的地址,不需要自己配置。