Compare commits

...

5 Commits

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
16 changed files with 241 additions and 148 deletions

View File

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

View File

@@ -9,90 +9,95 @@ interface StreamFetchProps {
abortSignal: AbortController;
}
export const streamFetch = ({ data, onMessage, abortSignal }: StreamFetchProps) =>
new Promise<ChatResponseType & { responseText: string }>(async (resolve, reject) => {
try {
const response = await window.fetch('/api/openapi/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
signal: abortSignal.signal,
body: JSON.stringify({
...data,
stream: true
})
});
new Promise<ChatResponseType & { responseText: string; errMsg: string }>(
async (resolve, reject) => {
try {
const response = await window.fetch('/api/openapi/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
signal: abortSignal.signal,
body: JSON.stringify({
...data,
stream: true
})
});
if (response.status !== 200) {
const err = await response.json();
return reject(err);
}
if (response.status !== 200) {
const err = await response.json();
return reject(err);
}
if (!response?.body) {
throw new Error('Request Error');
}
if (!response?.body) {
throw new Error('Request Error');
}
const reader = response.body?.getReader();
const reader = response.body?.getReader();
// response data
let responseText = '';
let newChatId = '';
let quoteLen = 0;
// response data
let responseText = '';
let newChatId = '';
let quoteLen = 0;
let errMsg = '';
const read = async () => {
try {
const { done, value } = await reader.read();
if (done) {
if (response.status === 200) {
const read = async () => {
try {
const { done, value } = await reader.read();
if (done) {
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({
responseText,
newChatId,
quoteLen
quoteLen,
errMsg
});
} else {
return reject('响应过程出现异常~');
}
reject(getErrText(err, '请求异常'));
}
const chunkResponse = parseStreamChunk(value);
};
read();
} catch (err: any) {
console.log(err);
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) {
return reject(getErrText(data, '流响应错误'));
}
});
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, '请求异常'));
reject(getErrText(err, '请求异常'));
}
}
});
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,10 +65,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
const modelConstantsData = ChatModelMap[model.chat.chatModel];
const prompt = prompts[prompts.length - 1];
const { userSystemPrompt = [], quotePrompt = [] } = await (async () => {
const {
userSystemPrompt = [],
userLimitPrompt = [],
quotePrompt = []
} = await (async () => {
// 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) {
const { userSystemPrompt, quotePrompt } = await appKbSearch({
const { quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
model,
userId,
fixedQuote: [],
@@ -78,21 +82,29 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
});
return {
userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [],
userSystemPrompt,
userLimitPrompt,
quotePrompt: [quotePrompt]
};
}
if (model.chat.systemPrompt) {
return {
userSystemPrompt: [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
};
}
return {};
return {
userSystemPrompt: model.chat.systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
: [],
userLimitPrompt: model.chat.limitPrompt
? [
{
obj: ChatRoleEnum.Human,
value: model.chat.limitPrompt
}
]
: []
};
})();
// 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(

View File

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

View File

@@ -108,11 +108,12 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
const {
rawSearch = [],
userSystemPrompt = [],
userLimitPrompt = [],
quotePrompt = []
} = await (async () => {
// 使用了知识库搜索
if (model.chat.relatedKbs?.length > 0) {
const { rawSearch, userSystemPrompt, quotePrompt } = await appKbSearch({
const { rawSearch, quotePrompt, userSystemPrompt, userLimitPrompt } = await appKbSearch({
model,
userId,
fixedQuote: history[history.length - 1]?.quote,
@@ -123,21 +124,29 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
return {
rawSearch,
userSystemPrompt: userSystemPrompt ? [userSystemPrompt] : [],
userSystemPrompt,
userLimitPrompt,
quotePrompt: [quotePrompt]
};
}
if (model.chat.systemPrompt) {
return {
userSystemPrompt: [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
};
}
return {};
return {
userSystemPrompt: model.chat.systemPrompt
? [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
]
: [],
userLimitPrompt: model.chat.limitPrompt
? [
{
obj: ChatRoleEnum.Human,
value: model.chat.limitPrompt
}
]
: []
};
})();
// search result is empty
@@ -167,7 +176,13 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}
// 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
const modelConstantsData = ChatModelMap[model.chat.chatModel];
// FastGpt temperature range: 1~10
@@ -176,7 +191,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
);
await sensitiveCheck({
input: `${prompt.value}`
input: `${userSystemPrompt[0]?.value}\n${userLimitPrompt[0]?.value}\n${prompt.value}`
});
// start model api. responseText and totalTokens: valid only if stream = false
@@ -259,7 +274,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
...(showAppDetail
? {
quote: rawSearch,
systemPrompt: userSystemPrompt?.[0]?.value
systemPrompt: `${userSystemPrompt[0]?.value}\n\n${userLimitPrompt[0]?.value}`
}
: {})
}

View File

@@ -174,7 +174,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
const messages = adaptChatItem_openAI({ messages: prompts, reserveId: true });
// 流请求,获取数据
const { newChatId, quoteLen } = await streamFetch({
const { newChatId, quoteLen, errMsg } = await streamFetch({
data: {
messages,
chatId,
@@ -219,7 +219,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
...item,
status: 'finish',
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 });
loadMyModels(true);
}, 100);
if (errMsg) {
toast({
status: 'warning',
title: errMsg
});
}
},
[
chatId,
modelId,
chatData.systemPrompt,
setChatData,
loadHistory,
loadMyModels,
generatingMessage,
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]}
onClick={() => setShowSystemPrompt(item.systemPrompt || '')}
>
&
</Button>
)}
{!!item.quoteLen && (

View File

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

View File

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

View File

@@ -238,6 +238,7 @@ export const V2_StreamResponse = async ({
}
if (error) {
console.log(error);
return Promise.reject(error);
}

View File

@@ -49,8 +49,8 @@ export const chatResponse = async ({
messages: adaptMessages,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream,
stop: ['.!?。']
stream
// stop: ['.!?。']
},
{
timeout: stream ? 60000 : 480000,

View File

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

View File

@@ -116,7 +116,7 @@ export const voiceBroadcast = ({ text }: { text: string }) => {
};
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);
return msg;
};