Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ff5db7b41 | ||
|
|
56a0b48b97 | ||
|
|
ff24042df5 | ||
|
|
c31d247f07 | ||
|
|
e903eb5b94 | ||
|
|
c605964fa8 | ||
|
|
1fe5cd751a | ||
|
|
488e2f476e | ||
|
|
915b104b8a | ||
|
|
aaa350a13e | ||
|
|
6a2b34cb92 |
@@ -8,3 +8,4 @@ README.md
|
||||
|
||||
.yalc/
|
||||
yalc.lock
|
||||
testApi/
|
||||
2
Makefile
2
Makefile
@@ -34,7 +34,7 @@ run: ## Run a dev service from host.
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build: ## Build docker image with the desktop-frontend.
|
||||
docker build -t c121914yu/fast-gpt:latest .
|
||||
docker build -t c121914yu/fast-gpt:latest . --network host --build-arg HTTP_PROXY=http://127.0.0.1:7890 --build-arg HTTPS_PROXY=http://127.0.0.1:7890
|
||||
|
||||
##@ Deployment
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
接受一个csv文件,表格头包含 question 和 answer。question 代表问题,answer 代表答案。
|
||||
导入前会进行去重,如果问题和答案完全相同,则不会被导入,所以最终导入的内容可能会比文件的内容少。
|
||||
导入前会进行去重,如果问题和答案完全相同,则不会被导入,所以最终导入的内容可能会比文件的内容少。但是,对于带有换行的内容,目前无法去重。
|
||||
| question | answer |
|
||||
| --- | --- |
|
||||
| 什么是 laf | laf 是一个云函数开发平台…… |
|
||||
|
||||
@@ -49,6 +49,7 @@ export const getModelTrainings = (id: string) =>
|
||||
|
||||
type GetModelDataListProps = RequestPaging & {
|
||||
modelId: string;
|
||||
searchText: string;
|
||||
};
|
||||
/**
|
||||
* 获取模型的知识库数据
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
}
|
||||
.markdown ul,
|
||||
.markdown ol {
|
||||
padding-left: 1em;
|
||||
padding-left: 2em;
|
||||
}
|
||||
.markdown ul.no-list,
|
||||
.markdown ol.no-list {
|
||||
|
||||
@@ -4,14 +4,12 @@ import type { RedisModelDataItemType } from '@/types/redis';
|
||||
export enum ChatModelNameEnum {
|
||||
GPT35 = 'gpt-3.5-turbo',
|
||||
VECTOR_GPT = 'VECTOR_GPT',
|
||||
GPT3 = 'text-davinci-003',
|
||||
VECTOR = 'text-embedding-ada-002'
|
||||
}
|
||||
|
||||
export const ChatModelNameMap = {
|
||||
[ChatModelNameEnum.GPT35]: 'gpt-3.5-turbo',
|
||||
[ChatModelNameEnum.VECTOR_GPT]: 'gpt-3.5-turbo',
|
||||
[ChatModelNameEnum.GPT3]: 'text-davinci-003',
|
||||
[ChatModelNameEnum.VECTOR]: 'text-embedding-ada-002'
|
||||
};
|
||||
|
||||
@@ -34,7 +32,7 @@ export const modelList: ModelConstantsData[] = [
|
||||
trainName: '',
|
||||
maxToken: 4000,
|
||||
contextMaxToken: 7500,
|
||||
maxTemperature: 2,
|
||||
maxTemperature: 1.5,
|
||||
price: 3
|
||||
},
|
||||
{
|
||||
@@ -47,16 +45,6 @@ export const modelList: ModelConstantsData[] = [
|
||||
maxTemperature: 1,
|
||||
price: 3
|
||||
}
|
||||
// {
|
||||
// serviceCompany: 'openai',
|
||||
// name: 'GPT3',
|
||||
// model: ChatModelNameEnum.GPT3,
|
||||
// trainName: 'davinci',
|
||||
// maxToken: 4000,
|
||||
// contextMaxToken: 7500,
|
||||
// maxTemperature: 2,
|
||||
// price: 30
|
||||
// }
|
||||
];
|
||||
|
||||
export enum TrainingStatusEnum {
|
||||
@@ -97,6 +85,34 @@ export const ModelDataStatusMap: Record<RedisModelDataItemType['status'], string
|
||||
waiting: '训练中'
|
||||
};
|
||||
|
||||
/* 知识库搜索时的配置 */
|
||||
// 搜索方式
|
||||
export enum ModelVectorSearchModeEnum {
|
||||
hightSimilarity = 'hightSimilarity', // 高相似度+禁止回复
|
||||
lowSimilarity = 'lowSimilarity', // 低相似度
|
||||
noContext = 'noContex' // 高相似度+无上下文回复
|
||||
}
|
||||
export const ModelVectorSearchModeMap: Record<
|
||||
`${ModelVectorSearchModeEnum}`,
|
||||
{
|
||||
text: string;
|
||||
similarity: number;
|
||||
}
|
||||
> = {
|
||||
[ModelVectorSearchModeEnum.hightSimilarity]: {
|
||||
text: '高相似度, 无匹配时拒绝回复',
|
||||
similarity: 0.2
|
||||
},
|
||||
[ModelVectorSearchModeEnum.noContext]: {
|
||||
text: '高相似度,无匹配时直接回复',
|
||||
similarity: 0.2
|
||||
},
|
||||
[ModelVectorSearchModeEnum.lowSimilarity]: {
|
||||
text: '低相似度匹配',
|
||||
similarity: 0.8
|
||||
}
|
||||
};
|
||||
|
||||
export const defaultModel: ModelSchema = {
|
||||
_id: '',
|
||||
userId: '',
|
||||
@@ -108,6 +124,9 @@ export const defaultModel: ModelSchema = {
|
||||
systemPrompt: '',
|
||||
intro: '',
|
||||
temperature: 5,
|
||||
search: {
|
||||
mode: ModelVectorSearchModeEnum.hightSimilarity
|
||||
},
|
||||
service: {
|
||||
company: 'openai',
|
||||
trainId: '',
|
||||
|
||||
@@ -7,12 +7,13 @@ import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { PassThrough } from 'stream';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { modelList, ModelVectorSearchModeMap, ModelVectorSearchModeEnum } from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { connectRedis } from '@/service/redis';
|
||||
import { VecModelDataPrefix } from '@/constants/redis';
|
||||
import { vectorToBuffer } from '@/utils/tools';
|
||||
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -64,13 +65,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
text: prompt.value
|
||||
});
|
||||
|
||||
const similarity = ModelVectorSearchModeMap[model.search.mode]?.similarity || 0.22;
|
||||
// 搜索系统提示词, 按相似度从 redis 中搜出相关的 q 和 text
|
||||
const redisData: any[] = await redis.sendCommand([
|
||||
'FT.SEARCH',
|
||||
`idx:${VecModelDataPrefix}:hash`,
|
||||
`@modelId:{${String(
|
||||
chat.modelId._id
|
||||
)}} @vector:[VECTOR_RANGE 0.24 $blob]=>{$YIELD_DISTANCE_AS: score}`,
|
||||
)}} @vector:[VECTOR_RANGE ${similarity} $blob]=>{$YIELD_DISTANCE_AS: score}`,
|
||||
'RETURN',
|
||||
'1',
|
||||
'text',
|
||||
@@ -96,17 +98,34 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if (formatRedisPrompt.length === 0) {
|
||||
throw new Error('对不起,我没有找到你的问题');
|
||||
/* 高相似度+退出,无法匹配时直接退出 */
|
||||
if (
|
||||
formatRedisPrompt.length === 0 &&
|
||||
model.search.mode === ModelVectorSearchModeEnum.hightSimilarity
|
||||
) {
|
||||
return res.send('对不起,你的问题不在知识库中。');
|
||||
}
|
||||
/* 高相似度+无上下文,不添加额外知识 */
|
||||
if (
|
||||
formatRedisPrompt.length === 0 &&
|
||||
model.search.mode === ModelVectorSearchModeEnum.noContext
|
||||
) {
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: model.systemPrompt
|
||||
});
|
||||
} else {
|
||||
// 有匹配情况下,添加知识库内容。
|
||||
// 系统提示词过滤,最多 2800 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2800);
|
||||
|
||||
// textArr 筛选,最多 2800 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2800);
|
||||
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: `${model.systemPrompt} 知识库内容是最新的,知识库内容为: "${systemPrompt}"`
|
||||
});
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: `${model.systemPrompt} 用知识库内容回答,知识库内容为: "当前时间:${dayjs().format(
|
||||
'YYYY/MM/DD HH:mm:ss'
|
||||
)} ${systemPrompt}"`
|
||||
});
|
||||
}
|
||||
|
||||
// 控制在 tokens 数量,防止超出
|
||||
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { connectToDatabase } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { connectRedis } from '@/service/redis';
|
||||
import { VecModelDataIdx } from '@/constants/redis';
|
||||
import { clearStrLineBreak } from '@/utils/tools';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
@@ -45,7 +44,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
searchRes.documents.forEach((item: any) => {
|
||||
if (item.value.q && item.value.text) {
|
||||
data.push([clearStrLineBreak(item.value.q), clearStrLineBreak(item.value.text)]);
|
||||
data.push([item.value.q.replace(/\n/g, '\\n'), item.value.text.replace(/\n/g, '\\n')]);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,20 +4,20 @@ import { connectToDatabase } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { connectRedis } from '@/service/redis';
|
||||
import { VecModelDataIdx } from '@/constants/redis';
|
||||
import { SearchOptions } from 'redis';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
let {
|
||||
modelId,
|
||||
pageNum = 1,
|
||||
pageSize = 10
|
||||
pageSize = 10,
|
||||
searchText = ''
|
||||
} = req.query as {
|
||||
modelId: string;
|
||||
pageNum: string;
|
||||
pageSize: string;
|
||||
searchText: string;
|
||||
};
|
||||
|
||||
const { authorization } = req.headers;
|
||||
|
||||
pageNum = +pageNum;
|
||||
@@ -40,7 +40,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
// 从 redis 中获取数据
|
||||
const searchRes = await redis.ft.search(
|
||||
VecModelDataIdx,
|
||||
`@modelId:{${modelId}} @userId:{${userId}}`,
|
||||
`@modelId:{${modelId}} @userId:{${userId}} ${searchText ? `*${searchText}*` : ''}`,
|
||||
{
|
||||
RETURN: ['q', 'text', 'status'],
|
||||
LIMIT: {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ModelUpdateParams } from '@/types/model';
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { name, service, security, systemPrompt, intro, temperature } =
|
||||
const { name, search, service, security, systemPrompt, intro, temperature } =
|
||||
req.body as ModelUpdateParams;
|
||||
const { modelId } = req.query as { modelId: string };
|
||||
const { authorization } = req.headers;
|
||||
@@ -37,6 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
systemPrompt,
|
||||
intro,
|
||||
temperature,
|
||||
search,
|
||||
// service,
|
||||
security
|
||||
}
|
||||
|
||||
158
src/pages/api/openapi/chat/chatGpt.ts
Normal file
158
src/pages/api/openapi/chat/chatGpt.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase, Model } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { httpsAgent, openaiChatFilter, authOpenApiKey } from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { PassThrough } from 'stream';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { gpt35StreamResponse } from '@/service/utils/openai';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
let step = 0; // step=1时,表示开始了流响应
|
||||
const stream = new PassThrough();
|
||||
stream.on('error', () => {
|
||||
console.log('error: ', 'stream error');
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('close', () => {
|
||||
stream.destroy();
|
||||
});
|
||||
res.on('error', () => {
|
||||
console.log('error: ', 'request error');
|
||||
stream.destroy();
|
||||
});
|
||||
|
||||
try {
|
||||
const {
|
||||
prompts,
|
||||
modelId,
|
||||
isStream = true
|
||||
} = req.body as {
|
||||
prompts: ChatItemType[];
|
||||
modelId: string;
|
||||
isStream: boolean;
|
||||
};
|
||||
|
||||
if (!prompts || !modelId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
if (!Array.isArray(prompts)) {
|
||||
throw new Error('prompts is not array');
|
||||
}
|
||||
if (prompts.length > 30 || prompts.length === 0) {
|
||||
throw new Error('prompts length range 1-30');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
let startTime = Date.now();
|
||||
|
||||
const { apiKey, userId } = await authOpenApiKey(req);
|
||||
|
||||
const model = await Model.findOne({
|
||||
_id: modelId,
|
||||
userId
|
||||
});
|
||||
|
||||
if (!model) {
|
||||
throw new Error('无权使用该模型');
|
||||
}
|
||||
|
||||
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
|
||||
if (!modelConstantsData) {
|
||||
throw new Error('模型加载异常');
|
||||
}
|
||||
|
||||
// 如果有系统提示词,自动插入
|
||||
if (model.systemPrompt) {
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: model.systemPrompt
|
||||
});
|
||||
}
|
||||
|
||||
// 控制在 tokens 数量,防止超出
|
||||
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||
|
||||
// 格式化文本内容成 chatgpt 格式
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
||||
(item: ChatItemType) => ({
|
||||
role: map[item.obj],
|
||||
content: item.value
|
||||
})
|
||||
);
|
||||
// console.log(formatPrompts);
|
||||
// 计算温度
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(apiKey);
|
||||
// 发出请求
|
||||
const chatResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature: temperature,
|
||||
// max_tokens: modelConstantsData.maxToken,
|
||||
messages: formatPrompts,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream: isStream,
|
||||
stop: ['.!?。']
|
||||
},
|
||||
{
|
||||
timeout: 40000,
|
||||
responseType: isStream ? 'stream' : 'json',
|
||||
httpsAgent: httpsAgent(true)
|
||||
}
|
||||
);
|
||||
|
||||
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
step = 1;
|
||||
let responseContent = '';
|
||||
|
||||
if (isStream) {
|
||||
const streamResponse = await gpt35StreamResponse({
|
||||
res,
|
||||
stream,
|
||||
chatResponse
|
||||
});
|
||||
responseContent = streamResponse.responseContent;
|
||||
} else {
|
||||
responseContent = chatResponse.data.choices?.[0]?.message?.content || '';
|
||||
jsonRes(res, {
|
||||
data: responseContent
|
||||
});
|
||||
}
|
||||
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
|
||||
// 只有使用平台的 key 才计费
|
||||
pushChatBill({
|
||||
isPay: true,
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
text: promptsContent + responseContent
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (step === 1) {
|
||||
// 直接结束流
|
||||
console.log('error,结束');
|
||||
stream.destroy();
|
||||
} else {
|
||||
res.status(500);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,25 +83,26 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
下面是一些例子:
|
||||
实现一个手机号发生注册验证码方法.
|
||||
1. 从 query 中获取 phone.
|
||||
2. 校验手机号格式是否正确,不正确返回{error: "手机号格式错误"}.
|
||||
2. 校验手机号格式是否正确,不正确则返回错误码501,原因为:手机号格式错误.
|
||||
3. 给 phone 发送一个短信验证码,验证码长度为6位字符串,内容为:你正在注册laf,验证码为:code.
|
||||
4. 数据库添加数据,表为"codes",内容为 {phone, code}.
|
||||
|
||||
实现根据手机号注册账号,需要验证手机验证码.
|
||||
1. 从 body 中获取 phone 和 code.
|
||||
2. 校验手机号格式是否正确,不正确返回{error: "手机号格式错误"}.
|
||||
2. 获取数据库数据,表为"codes",查找是否有符合 phone, code 等于body参数的记录,没有的话返回 {error:"验证码不正确"}.
|
||||
2. 校验手机号格式是否正确,不正确则返回错误码501,原因为:手机号格式错误.
|
||||
2. 获取数据库数据,表为"codes",查找是否有符合 phone, code 等于body参数的记录,没有的话返回错误码500,原因为:验证码不正确.
|
||||
4. 添加数据库数据,表为"users" ,内容为{phone, code, createTime}.
|
||||
5. 删除数据库数据,删除 code 记录.
|
||||
6. 返回新建用户的Id: return {userId}
|
||||
|
||||
更新博客记录。传入blogId,blogText,tags,还需要记录更新的时间.
|
||||
1. 从 body 中获取 blogId,blogText 和 tags.
|
||||
2. 校验 blogId 是否为空,为空则返回 {error: "博客ID不能为空"}.
|
||||
3. 校验 blogText 是否为空,为空则返回 {error: "博客内容不能为空"}.
|
||||
4. 校验 tags 是否为数组,不是则返回 {error: "标签必须为数组"}.
|
||||
2. 校验 blogId 是否为空,为空则返回错误码500,原因为:博客ID不能为空.
|
||||
3. 校验 blogText 是否为空,为空则返回错误码500,原因为:博客内容不能为空.
|
||||
4. 校验 tags 是否为数组,不是则返回错误码500,原因为:标签必须为数组.
|
||||
5. 获取当前时间,记录为 updateTime.
|
||||
6. 更新数据库数据,表为"blogs",更新符合 blogId 的记录的内容为{blogText, tags, updateTime}.
|
||||
7. 返回结果 {message: "更新博客记录成功"}.`
|
||||
7. 返回结果 "更新博客记录成功"`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
@@ -161,8 +162,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// textArr 筛选,最多 3200 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 3200);
|
||||
// textArr 筛选,最多 3000 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 3000);
|
||||
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
|
||||
@@ -10,12 +10,13 @@ import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } fr
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { PassThrough } from 'stream';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { modelList, ModelVectorSearchModeMap, ModelVectorSearchModeEnum } from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { connectRedis } from '@/service/redis';
|
||||
import { VecModelDataPrefix } from '@/constants/redis';
|
||||
import { vectorToBuffer } from '@/utils/tools';
|
||||
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -83,11 +84,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
text: prompts[prompts.length - 1].value // 取最后一个
|
||||
});
|
||||
|
||||
// 搜索系统提示词, 按相似度从 redis 中搜出相关的 q 和 text
|
||||
const similarity = ModelVectorSearchModeMap[model.search.mode]?.similarity || 0.22;
|
||||
// 搜索系统提示词, 按相似度从 redis 中搜出相关的 q 和 text
|
||||
const redisData: any[] = await redis.sendCommand([
|
||||
'FT.SEARCH',
|
||||
`idx:${VecModelDataPrefix}:hash`,
|
||||
`@modelId:{${modelId}} @vector:[VECTOR_RANGE 0.24 $blob]=>{$YIELD_DISTANCE_AS: score}`,
|
||||
`@modelId:{${modelId}} @vector:[VECTOR_RANGE ${similarity} $blob]=>{$YIELD_DISTANCE_AS: score}`,
|
||||
'RETURN',
|
||||
'1',
|
||||
'text',
|
||||
@@ -114,22 +117,39 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if (formatRedisPrompt.length === 0) {
|
||||
throw new Error('对不起,我没有找到你的问题');
|
||||
}
|
||||
|
||||
// system 合并
|
||||
if (prompts[0].obj === 'SYSTEM') {
|
||||
formatRedisPrompt.unshift(prompts.shift()?.value || '');
|
||||
}
|
||||
|
||||
// 系统提示词筛选,最多 2800 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2800);
|
||||
/* 高相似度+退出,无法匹配时直接退出 */
|
||||
if (
|
||||
formatRedisPrompt.length === 0 &&
|
||||
model.search.mode === ModelVectorSearchModeEnum.hightSimilarity
|
||||
) {
|
||||
return res.send('对不起,你的问题不在知识库中。');
|
||||
}
|
||||
/* 高相似度+无上下文,不添加额外知识 */
|
||||
if (
|
||||
formatRedisPrompt.length === 0 &&
|
||||
model.search.mode === ModelVectorSearchModeEnum.noContext
|
||||
) {
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: model.systemPrompt
|
||||
});
|
||||
} else {
|
||||
// 有匹配或者低匹配度模式情况下,添加知识库内容。
|
||||
// 系统提示词过滤,最多 2800 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2800);
|
||||
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: `${model.systemPrompt} 知识库内容是最新的,知识库内容为: "${systemPrompt}"`
|
||||
});
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: `${model.systemPrompt} 用知识库内容回答,知识库内容为: "当前时间:${dayjs().format(
|
||||
'YYYY/MM/DD HH:mm:ss'
|
||||
)} ${systemPrompt}"`
|
||||
});
|
||||
}
|
||||
|
||||
// 控制在 tokens 数量,防止超出
|
||||
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||
|
||||
@@ -35,13 +35,11 @@ import WxConcat from '@/components/WxConcat';
|
||||
import { useMarkdown } from '@/hooks/useMarkdown';
|
||||
|
||||
const SlideBar = ({
|
||||
name,
|
||||
chatId,
|
||||
modelId,
|
||||
resetChat,
|
||||
onClose
|
||||
}: {
|
||||
name?: string;
|
||||
chatId: string;
|
||||
modelId: string;
|
||||
resetChat: () => void;
|
||||
@@ -188,14 +186,14 @@ const SlideBar = ({
|
||||
}}
|
||||
fontSize={'xs'}
|
||||
border={'1px solid transparent'}
|
||||
{...(item.name === name
|
||||
{...(item._id === modelId
|
||||
? {
|
||||
borderColor: 'rgba(255,255,255,0.5)',
|
||||
backgroundColor: 'rgba(255,255,255,0.1)'
|
||||
}
|
||||
: {})}
|
||||
onClick={async () => {
|
||||
if (item.name === name) return;
|
||||
if (item._id === modelId) return;
|
||||
router.replace(`/chat?chatId=${await getChatSiteId(item._id)}`);
|
||||
onClose();
|
||||
}}
|
||||
|
||||
@@ -114,8 +114,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
async (prompts: ChatSiteItemType) => {
|
||||
const urlMap: Record<string, string> = {
|
||||
[ChatModelNameEnum.GPT35]: '/api/chat/chatGpt',
|
||||
[ChatModelNameEnum.VECTOR_GPT]: '/api/chat/vectorGpt',
|
||||
[ChatModelNameEnum.GPT3]: '/api/chat/gpt3'
|
||||
[ChatModelNameEnum.VECTOR_GPT]: '/api/chat/vectorGpt'
|
||||
};
|
||||
|
||||
if (!urlMap[chatData.modelName]) return Promise.reject('找不到模型');
|
||||
@@ -362,7 +361,6 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
<Box flex={'0 0 250px'} w={0} h={'100%'}>
|
||||
<SlideBar
|
||||
resetChat={resetChat}
|
||||
name={chatData?.name}
|
||||
chatId={chatId}
|
||||
modelId={chatData.modelId}
|
||||
onClose={onCloseSlider}
|
||||
@@ -394,7 +392,6 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
<DrawerContent maxWidth={'250px'}>
|
||||
<SlideBar
|
||||
resetChat={resetChat}
|
||||
name={chatData?.name}
|
||||
chatId={chatId}
|
||||
modelId={chatData.modelId}
|
||||
onClose={onCloseSlider}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { useTabs } from '@/hooks/useTabs';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { readTxtContent, readPdfContent, readDocContent } from '@/utils/tools';
|
||||
import { readTxtContent, readPdfContent, readDocContent } from '@/utils/file';
|
||||
import { postSplitData } from '@/api/data';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
|
||||
@@ -124,7 +124,9 @@ const InputDataModal = ({
|
||||
<Box flex={2} mr={[0, 4]} mb={[4, 0]} h={['230px', '100%']}>
|
||||
<Box h={'30px'}>问题</Box>
|
||||
<Textarea
|
||||
placeholder="相关问题,可以回车输入多个问法, 最多500字"
|
||||
placeholder={
|
||||
'相关问题,可以输入多个问法, 最多500字。例如:\n1. laf 是什么?\n2. laf 可以做什么?\n3. laf怎么用'
|
||||
}
|
||||
maxLength={500}
|
||||
resize={'none'}
|
||||
h={'calc(100% - 30px)'}
|
||||
@@ -136,7 +138,9 @@ const InputDataModal = ({
|
||||
<Box flex={3} h={['330px', '100%']}>
|
||||
<Box h={'30px'}>知识点</Box>
|
||||
<Textarea
|
||||
placeholder="知识点,最多1000字"
|
||||
placeholder={
|
||||
'知识点,最多1000字。请保持主语的完整性,缺少主语会导致效果不佳。例如:\n1. laf是一个云函数开发平台。\n2. laf 什么都能做。\n3. 下面是使用 laf 的例子: ……'
|
||||
}
|
||||
maxLength={1000}
|
||||
resize={'none'}
|
||||
h={'calc(100% - 30px)'}
|
||||
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem
|
||||
MenuItem,
|
||||
Input
|
||||
} from '@chakra-ui/react';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import type { RedisModelDataItemType } from '@/types/redis';
|
||||
@@ -40,9 +41,11 @@ const SelectFileModel = dynamic(() => import('./SelectFileModal'));
|
||||
const SelectUrlModel = dynamic(() => import('./SelectUrlModal'));
|
||||
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
|
||||
|
||||
let lastSearch = '';
|
||||
|
||||
const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const {
|
||||
data: modelDataList,
|
||||
isLoading,
|
||||
@@ -54,7 +57,8 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
api: getModelDataList,
|
||||
pageSize: 8,
|
||||
params: {
|
||||
modelId: model._id
|
||||
modelId: model._id,
|
||||
searchText
|
||||
}
|
||||
});
|
||||
|
||||
@@ -152,15 +156,39 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
>
|
||||
手动输入
|
||||
</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectFileModal}>文本内容 QA 拆分</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectFileModal}>文本/文件 QA 拆分</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectUrlModal}>网站内容 QA 拆分</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectCsvModal}>csv 问答对导入</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Flex>
|
||||
{!!(splitDataLen && splitDataLen > 0) && (
|
||||
<Box fontSize={'xs'}>{splitDataLen}条数据正在拆分...</Box>
|
||||
)}
|
||||
<Flex mt={4}>
|
||||
{/* 拆分数据提示 */}
|
||||
{!!(splitDataLen && splitDataLen > 0) && (
|
||||
<Box fontSize={'xs'}>{splitDataLen}条数据正在拆分...</Box>
|
||||
)}
|
||||
<Box flex={1}></Box>
|
||||
<Input
|
||||
maxW={'240px'}
|
||||
size={'sm'}
|
||||
value={searchText}
|
||||
placeholder="搜索相关问题和答案,回车确认"
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onBlur={() => {
|
||||
if (searchText === lastSearch) return;
|
||||
getData(1);
|
||||
lastSearch = searchText;
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (searchText === lastSearch) return;
|
||||
if (e.key === 'Enter') {
|
||||
getData(1);
|
||||
lastSearch = searchText;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Box mt={4}>
|
||||
<TableContainer minH={'500px'}>
|
||||
<Table variant={'simple'}>
|
||||
|
||||
@@ -12,12 +12,13 @@ import {
|
||||
SliderThumb,
|
||||
SliderMark,
|
||||
Tooltip,
|
||||
Button
|
||||
Button,
|
||||
Select
|
||||
} from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { modelList } from '@/constants/model';
|
||||
import { modelList, ModelVectorSearchModeMap } from '@/constants/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
|
||||
@@ -53,20 +54,20 @@ const ModelEditForm = ({
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 80px'} w={0}>
|
||||
modelId:
|
||||
</Box>
|
||||
<Box>{getValues('_id')}</Box>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 80px'} w={0}>
|
||||
底层模型:
|
||||
模型类型:
|
||||
</Box>
|
||||
<Box>{getValues('service.modelName')}</Box>
|
||||
<Box>{modelList.find((item) => item.model === getValues('service.modelName'))?.name}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 80px'} w={0}>
|
||||
价格:
|
||||
</Box>
|
||||
@@ -79,7 +80,7 @@ const ModelEditForm = ({
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>删除:</Box>
|
||||
<Box flex={'0 0 150px'}>删除模型和数据集</Box>
|
||||
<Button
|
||||
colorScheme={'gray'}
|
||||
variant={'outline'}
|
||||
@@ -89,15 +90,6 @@ const ModelEditForm = ({
|
||||
删除模型
|
||||
</Button>
|
||||
</Flex>
|
||||
{/* <FormControl mt={4}>
|
||||
<Box mb={1}>介绍:</Box>
|
||||
<Textarea
|
||||
rows={5}
|
||||
maxLength={500}
|
||||
{...register('intro')}
|
||||
placeholder={'模型的介绍,仅做展示,不影响模型的效果'}
|
||||
/>
|
||||
</FormControl> */}
|
||||
</Card>
|
||||
<Card p={4}>
|
||||
<Box fontWeight={'bold'}>模型效果</Box>
|
||||
@@ -143,6 +135,20 @@ const ModelEditForm = ({
|
||||
</Slider>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
{canTrain && (
|
||||
<FormControl mt={4}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>搜索模式</Box>
|
||||
<Select {...register('search.mode', { required: '搜索模式不能为空' })}>
|
||||
{Object.entries(ModelVectorSearchModeMap).map(([key, { text }]) => (
|
||||
<option key={key} value={key}>
|
||||
{text}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
)}
|
||||
<Box mt={4}>
|
||||
<Box mb={1}>系统提示词</Box>
|
||||
<Textarea
|
||||
@@ -151,8 +157,8 @@ const ModelEditForm = ({
|
||||
{...register('systemPrompt')}
|
||||
placeholder={
|
||||
canTrain
|
||||
? '训练的模型会根据知识库内容,生成一部分系统提示词,因此在对话时需要消耗更多的 tokens。你仍可以增加一些提示词,让其效果更精确。'
|
||||
: '模型默认的 prompt 词,通过调整该内容,可以生成一个限定范围的模型。\n\n注意,改功能会影响对话的整体朝向!'
|
||||
? '训练的模型会根据知识库内容,生成一部分系统提示词,因此在对话时需要消耗更多的 tokens。你可以增加提示词,让效果更符合预期。例如: \n1. 请根据知识库内容回答用户问题。\n2. 知识库是电影《铃芽之旅》的内容,根据知识库内容回答。无关问题,拒绝回复!'
|
||||
: '模型默认的 prompt 词,通过调整该内容,可以生成一个限定范围的模型。\n注意,改功能会影响对话的整体朝向!'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -21,7 +21,6 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
const { isPc, media } = useScreen();
|
||||
const { setLoading } = useGlobalStore();
|
||||
|
||||
// const SelectFileDom = useRef<HTMLInputElement>(null);
|
||||
const [model, setModel] = useState<ModelSchema>(defaultModel);
|
||||
const formHooks = useForm<ModelSchema>({
|
||||
defaultValues: model
|
||||
@@ -143,6 +142,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
systemPrompt: data.systemPrompt,
|
||||
intro: data.intro,
|
||||
temperature: data.temperature,
|
||||
search: data.search,
|
||||
service: data.service,
|
||||
security: data.security
|
||||
});
|
||||
@@ -242,11 +242,6 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
<Grid mt={5} gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
|
||||
<ModelEditForm formHooks={formHooks} handleDelModel={handleDelModel} canTrain={canTrain} />
|
||||
|
||||
{/* {canTrain && (
|
||||
<Card p={4}>
|
||||
<Training model={model} />
|
||||
</Card>
|
||||
)} */}
|
||||
{canTrain && model._id && (
|
||||
<Card
|
||||
p={4}
|
||||
@@ -262,11 +257,6 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
</Card>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* 文件选择 */}
|
||||
{/* <Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
|
||||
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
|
||||
</Box> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { formatModelStatus } from '@/constants/model';
|
||||
import dayjs from 'dayjs';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { useRouter } from 'next/router';
|
||||
import { modelList } from '@/constants/model';
|
||||
|
||||
const ModelTable = ({
|
||||
models = [],
|
||||
@@ -31,6 +32,15 @@ const ModelTable = ({
|
||||
key: 'name',
|
||||
dataIndex: 'name'
|
||||
},
|
||||
{
|
||||
title: '模型类型',
|
||||
key: 'service',
|
||||
render: (model: ModelSchema) => (
|
||||
<Box fontWeight={'bold'} whiteSpace={'pre-wrap'} maxW={'200px'}>
|
||||
{modelList.find((item) => item.model === model.service.modelName)?.name}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '最后更新时间',
|
||||
key: 'updateTime',
|
||||
@@ -51,15 +61,7 @@ const ModelTable = ({
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'AI模型',
|
||||
key: 'service',
|
||||
render: (item: ModelSchema) => (
|
||||
<Box wordBreak={'break-all'} whiteSpace={'pre-wrap'} maxW={'200px'}>
|
||||
{item.service.modelName}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
title: '操作',
|
||||
key: 'control',
|
||||
@@ -69,7 +71,7 @@ const ModelTable = ({
|
||||
对话
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme={'gray'}
|
||||
variant={'outline'}
|
||||
onClick={() => router.push(`/model/detail?modelId=${item._id}`)}
|
||||
>
|
||||
编辑
|
||||
|
||||
@@ -30,7 +30,7 @@ const BillTable = () => {
|
||||
<Th>类型</Th>
|
||||
<Th>内容长度</Th>
|
||||
<Th>Tokens 长度</Th>
|
||||
<Th>消费</Th>
|
||||
<Th>金额</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
|
||||
@@ -77,7 +77,7 @@ const PayRecordTable = () => {
|
||||
<Th>订单号</Th>
|
||||
<Th>时间</Th>
|
||||
<Th>金额</Th>
|
||||
<Th>消费</Th>
|
||||
<Th>状态</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
|
||||
@@ -8,7 +8,7 @@ export const openaiError: Record<string, string> = {
|
||||
};
|
||||
export const openaiError2: Record<string, string> = {
|
||||
insufficient_quota: 'API 余额不足',
|
||||
invalid_request_error: '输入参数异常'
|
||||
invalid_request_error: 'openai 接口异常'
|
||||
};
|
||||
export const proxyError: Record<string, boolean> = {
|
||||
ECONNABORTED: true,
|
||||
|
||||
@@ -21,7 +21,7 @@ export const pushChatBill = async ({
|
||||
|
||||
try {
|
||||
// 计算 token 数量
|
||||
const tokens = Math.floor(encode(text).length * 0.7);
|
||||
const tokens = Math.floor(encode(text).length * 0.75);
|
||||
|
||||
console.log(`chat generate success. text len: ${text.length}. token len: ${tokens}`);
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Schema, model, models, Model as MongoModel } from 'mongoose';
|
||||
import { ModelSchema as ModelType } from '@/types/mongoSchema';
|
||||
import { ModelVectorSearchModeMap, ModelVectorSearchModeEnum } from '@/constants/model';
|
||||
|
||||
const ModelSchema = new Schema({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
@@ -43,6 +45,13 @@ const ModelSchema = new Schema({
|
||||
max: 10,
|
||||
default: 4
|
||||
},
|
||||
search: {
|
||||
mode: {
|
||||
type: String,
|
||||
enum: Object.keys(ModelVectorSearchModeMap),
|
||||
default: ModelVectorSearchModeEnum.hightSimilarity
|
||||
}
|
||||
},
|
||||
service: {
|
||||
company: {
|
||||
type: String,
|
||||
|
||||
@@ -49,6 +49,6 @@ export const jsonRes = <T = any>(
|
||||
code,
|
||||
statusText: '',
|
||||
message: msg,
|
||||
data: data || null
|
||||
data: data !== undefined ? data : null
|
||||
});
|
||||
};
|
||||
|
||||
@@ -91,7 +91,7 @@ export const openaiChatFilter = (prompts: ChatItemType[], maxTokens: number) =>
|
||||
const formatPrompts = prompts.map((item) => ({
|
||||
obj: item.obj,
|
||||
value: item.value
|
||||
.replace(/[\u3000\u3001\uff01-\uff5e\u3002]/g, ' ') // 中文标点改空格
|
||||
// .replace(/[\u3000\u3001\uff01-\uff5e\u3002]/g, ' ') // 中文标点改空格
|
||||
.replace(/\n+/g, '\n') // 连续空行
|
||||
.replace(/[^\S\r\n]+/g, ' ') // 连续空白内容
|
||||
.trim()
|
||||
|
||||
@@ -49,6 +49,11 @@ svg {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
|
||||
5
src/types/model.d.ts
vendored
5
src/types/model.d.ts
vendored
@@ -5,8 +5,9 @@ export interface ModelUpdateParams {
|
||||
systemPrompt: string;
|
||||
intro: string;
|
||||
temperature: number;
|
||||
service: ModelSchema.service;
|
||||
security: ModelSchema.security;
|
||||
search: ModelSchema['search'];
|
||||
service: ModelSchema['service'];
|
||||
security: ModelSchema['security'];
|
||||
}
|
||||
|
||||
export interface ModelDataItemType {
|
||||
|
||||
10
src/types/mongoSchema.d.ts
vendored
10
src/types/mongoSchema.d.ts
vendored
@@ -1,5 +1,10 @@
|
||||
import type { ChatItemType } from './chat';
|
||||
import { ModelStatusEnum, TrainingStatusEnum, ChatModelNameEnum } from '@/constants/model';
|
||||
import {
|
||||
ModelStatusEnum,
|
||||
TrainingStatusEnum,
|
||||
ChatModelNameEnum,
|
||||
ModelVectorSearchModeEnum
|
||||
} from '@/constants/model';
|
||||
import type { DataType } from './data';
|
||||
|
||||
export type ServiceName = 'openai';
|
||||
@@ -32,6 +37,9 @@ export interface ModelSchema {
|
||||
updateTime: number;
|
||||
trainingTimes: number;
|
||||
temperature: number;
|
||||
search: {
|
||||
mode: `${ModelVectorSearchModeEnum}`;
|
||||
};
|
||||
service: {
|
||||
company: ServiceName;
|
||||
trainId: string; // 训练的模型,训练后就是训练的模型id
|
||||
|
||||
@@ -125,7 +125,7 @@ export const fileDownload = ({
|
||||
filename: string;
|
||||
}) => {
|
||||
// 导出为文件
|
||||
const blob = new Blob([text], { type: `${type};charset=utf-8` });
|
||||
const blob = new Blob([`\uFEFF${text}`], { type: `${type};charset=utf-8;` });
|
||||
|
||||
// 创建下载链接
|
||||
const downloadLink = document.createElement('a');
|
||||
|
||||
@@ -71,10 +71,3 @@ export const formatVector = (vector: number[]) => {
|
||||
|
||||
return formattedVector;
|
||||
};
|
||||
|
||||
/**
|
||||
* 字符串清理,替换换行符号
|
||||
*/
|
||||
export const clearStrLineBreak = (str: string) => {
|
||||
return str.replace(/\n+/g, '\n').replace(/\n/g, '\\n').trim();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user