mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 05:12:39 +00:00
185 lines
5.1 KiB
TypeScript
185 lines
5.1 KiB
TypeScript
import { countGptMessagesTokens } from '../../common/string/tiktoken/index';
|
|
import type {
|
|
ChatCompletionContentPart,
|
|
ChatCompletionMessageParam
|
|
} from '@fastgpt/global/core/ai/type.d';
|
|
import axios from 'axios';
|
|
import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants';
|
|
import { guessBase64ImageType } from '../../common/file/utils';
|
|
import { serverRequestBaseUrl } from '../../common/api/serverRequest';
|
|
import { cloneDeep } from 'lodash';
|
|
|
|
/* slice chat context by tokens */
|
|
const filterEmptyMessages = (messages: ChatCompletionMessageParam[]) => {
|
|
return messages.filter((item) => {
|
|
if (item.role === ChatCompletionRequestMessageRoleEnum.System) return !!item.content;
|
|
if (item.role === ChatCompletionRequestMessageRoleEnum.User) return !!item.content;
|
|
if (item.role === ChatCompletionRequestMessageRoleEnum.Assistant)
|
|
return !!item.content || !!item.function_call || !!item.tool_calls;
|
|
return true;
|
|
});
|
|
};
|
|
|
|
export const filterGPTMessageByMaxTokens = async ({
|
|
messages = [],
|
|
maxTokens
|
|
}: {
|
|
messages: ChatCompletionMessageParam[];
|
|
maxTokens: number;
|
|
}) => {
|
|
if (!Array.isArray(messages)) {
|
|
return [];
|
|
}
|
|
const rawTextLen = messages.reduce((sum, item) => {
|
|
if (typeof item.content === 'string') {
|
|
return sum + item.content.length;
|
|
}
|
|
if (Array.isArray(item.content)) {
|
|
return (
|
|
sum +
|
|
item.content.reduce((sum, item) => {
|
|
if (item.type === 'text') {
|
|
return sum + item.text.length;
|
|
}
|
|
return sum;
|
|
}, 0)
|
|
);
|
|
}
|
|
return sum;
|
|
}, 0);
|
|
|
|
// If the text length is less than half of the maximum token, no calculation is required
|
|
if (rawTextLen < maxTokens * 0.5) {
|
|
return filterEmptyMessages(messages);
|
|
}
|
|
|
|
// filter startWith system prompt
|
|
const chatStartIndex = messages.findIndex(
|
|
(item) => item.role !== ChatCompletionRequestMessageRoleEnum.System
|
|
);
|
|
const systemPrompts: ChatCompletionMessageParam[] = messages.slice(0, chatStartIndex);
|
|
const chatPrompts: ChatCompletionMessageParam[] = messages.slice(chatStartIndex);
|
|
|
|
// reduce token of systemPrompt
|
|
maxTokens -= await countGptMessagesTokens(systemPrompts);
|
|
|
|
// Save the last chat prompt(question)
|
|
const question = chatPrompts.pop();
|
|
if (!question) {
|
|
return systemPrompts;
|
|
}
|
|
const chats: ChatCompletionMessageParam[] = [question];
|
|
|
|
// 从后往前截取对话内容, 每次需要截取2个
|
|
while (1) {
|
|
const assistant = chatPrompts.pop();
|
|
const user = chatPrompts.pop();
|
|
if (!assistant || !user) {
|
|
break;
|
|
}
|
|
|
|
const tokens = await countGptMessagesTokens([assistant, user]);
|
|
maxTokens -= tokens;
|
|
/* 整体 tokens 超出范围,截断 */
|
|
if (maxTokens < 0) {
|
|
break;
|
|
}
|
|
|
|
chats.unshift(assistant);
|
|
chats.unshift(user);
|
|
|
|
if (chatPrompts.length === 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return filterEmptyMessages([...systemPrompts, ...chats]);
|
|
};
|
|
|
|
export const formatGPTMessagesInRequestBefore = (messages: ChatCompletionMessageParam[]) => {
|
|
return messages
|
|
.map((item) => {
|
|
if (!item.content) return;
|
|
if (typeof item.content === 'string') {
|
|
return {
|
|
...item,
|
|
content: item.content.trim()
|
|
};
|
|
}
|
|
|
|
// array
|
|
if (item.content.length === 0) return;
|
|
if (item.content.length === 1 && item.content[0].type === 'text') {
|
|
return {
|
|
...item,
|
|
content: item.content[0].text
|
|
};
|
|
}
|
|
|
|
return item;
|
|
})
|
|
.filter(Boolean) as ChatCompletionMessageParam[];
|
|
};
|
|
|
|
/* Load user chat content.
|
|
Img: to base 64
|
|
*/
|
|
export const loadChatImgToBase64 = async (content: string | ChatCompletionContentPart[]) => {
|
|
if (typeof content === 'string') {
|
|
return content;
|
|
}
|
|
|
|
return Promise.all(
|
|
content.map(async (item) => {
|
|
if (item.type === 'text') return item;
|
|
|
|
if (!item.image_url.url) return item;
|
|
|
|
/*
|
|
1. From db: Get it from db
|
|
2. From web: Not update
|
|
*/
|
|
if (item.image_url.url.startsWith('/')) {
|
|
const response = await axios.get(item.image_url.url, {
|
|
baseURL: serverRequestBaseUrl,
|
|
responseType: 'arraybuffer'
|
|
});
|
|
const base64 = Buffer.from(response.data).toString('base64');
|
|
let imageType = response.headers['content-type'];
|
|
if (imageType === undefined) {
|
|
imageType = guessBase64ImageType(base64);
|
|
}
|
|
return {
|
|
...item,
|
|
image_url: {
|
|
...item.image_url,
|
|
url: `data:${imageType};base64,${base64}`
|
|
}
|
|
};
|
|
}
|
|
|
|
return item;
|
|
})
|
|
);
|
|
};
|
|
export const loadRequestMessages = async (messages: ChatCompletionMessageParam[]) => {
|
|
if (messages.length === 0) {
|
|
return Promise.reject('core.chat.error.Messages empty');
|
|
}
|
|
|
|
const loadMessages = await Promise.all(
|
|
messages.map(async (item) => {
|
|
if (item.role === ChatCompletionRequestMessageRoleEnum.User) {
|
|
return {
|
|
...item,
|
|
content: await loadChatImgToBase64(item.content)
|
|
};
|
|
} else {
|
|
return item;
|
|
}
|
|
})
|
|
);
|
|
|
|
return loadMessages;
|
|
};
|