mirror of
https://github.com/labring/FastGPT.git
synced 2025-12-25 20:02:47 +00:00
Some checks are pending
Document deploy / sync-images (push) Waiting to run
Document deploy / generate-timestamp (push) Blocked by required conditions
Document deploy / build-images (map[domain:https://fastgpt.cn suffix:cn]) (push) Blocked by required conditions
Document deploy / build-images (map[domain:https://fastgpt.io suffix:io]) (push) Blocked by required conditions
Document deploy / update-images (map[deployment:fastgpt-docs domain:https://fastgpt.cn kube_config:KUBE_CONFIG_CN suffix:cn]) (push) Blocked by required conditions
Document deploy / update-images (map[deployment:fastgpt-docs domain:https://fastgpt.io kube_config:KUBE_CONFIG_IO suffix:io]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / get-vars (push) Waiting to run
Build FastGPT images in Personal warehouse / build-fastgpt-images (map[arch:amd64 runs-on:ubuntu-24.04]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / build-fastgpt-images (map[arch:arm64 runs-on:ubuntu-24.04-arm]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / release-fastgpt-images (push) Blocked by required conditions
* feat: migrate chat files to s3 (#5802) * feat: migrate chat files to s3 * feat: add delete jobs for deleting s3 files * chore: improvements * fix: lockfile * fix: imports * feat: add ttl for those uploaded files but not send yet * feat: init bullmq worker * fix: s3 key * perf: s3 internal url * remove env * fix: re-sign a new url * fix: re-sign a new url * perf: s3 code --------- Co-authored-by: archer <545436317@qq.com> * update pacakge * feat: add more file type for uploading (#5807) * fix: re-sign a new url * wip: file selector * feat: add more file type for uploading * feat: migrate chat files to s3 (#5802) * feat: migrate chat files to s3 * feat: add delete jobs for deleting s3 files * chore: improvements * fix: lockfile * fix: imports * feat: add ttl for those uploaded files but not send yet * feat: init bullmq worker * fix: s3 key * perf: s3 internal url * remove env * fix: re-sign a new url * fix: re-sign a new url * perf: s3 code --------- Co-authored-by: archer <545436317@qq.com> * fix: limit minmax available file upload number * perf: file select modal code * fix: fileselect refresh * fix: ts --------- Co-authored-by: archer <545436317@qq.com> * bugfix: chat page (#5809) * fix: upload avatar * fix: chat page username display issue and setting button visibility * doc * Markdown match base64 performance * feat: improve global variables(time, file, dataset) (#5804) * feat: improve global variables(time, file, dataset) * feat: optimize code * perf: time variables code * fix: model, file * fix: hide file upload * fix: ts * hide dataset select --------- Co-authored-by: archer <545436317@qq.com> * perf: insert training queue * perf: s3 upload error i18n * fix: share page s3 * fix: timeselector ui error * var update node * Timepicker ui * feat: plugin support password * fix: password disabled UX * fix: button size * fix: no model cache for chat page (#5820) * rename function * fix: workflow bug * fix: interactive loop * fix test * perf: common textare no richtext * move system plugin config (#5803) (#5813) * move system plugin config (#5803) * move system plugin config * extract tag bar * filter * tool detail temp * marketplace * params * fix * type * search * tags render * status * ui * code * connect to backend (#5815) * feat: marketplace apis & type definitions (#5817) * chore: marketplace init * chore: marketplace list api type * chore: detail api * marketplace & import * feat: marketplace ui (#5826) * temp * marketplace * import * feat: detail return readme * chore: cache data expire 10 mins * chore: update docs * feat: marketplace ui --------- Co-authored-by: heheer <zhiyu44@qq.com> * feat: marketplace (#5830) * temp * marketplace * chore: tool list tag filter * chore: adjust --------- Co-authored-by: heheer <zhiyu44@qq.com> * tool detail drawer * remove tag filter * fix * fix * fix build * update pnpm-lock * fix type * perf code * marketplace router * fix build * navbar icon * fix ui * fix init * docs: marketplace/plugin (#5832) * temp * marketplace * docs(plugin): system tool docs --------- Co-authored-by: heheer <zhiyu44@qq.com> * default url * feat: i18n/ docker build (#5833) * chore: docker build * feat: i18n selector * fix * fix * fix: i18n parse * fix: i18n parse --------- Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> Co-authored-by: heheer <zhiyu44@qq.com> * marketplace url * update action * market place code * market place code * title * fix: nextconfig * fix: copilot review * Remove bypassable regex-based XSS sanitization from marketplace search (#5835) * Initial plan * Remove problematic regex-based XSS sanitization from search inputs Co-authored-by: c121914yu <50446880+c121914yu@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: c121914yu <50446880+c121914yu@users.noreply.github.com> * feat: tool tag openapi * api check * fix: tsc * fix: ts * fix: lock * sdk version * ts * sdk version * remove invalid tip * perf: export data add timezone * perf: admin plugin api move * perf: tool code * move tag code * perf: marketplace and team plugin code * remove workflow invalid request * rename global tool code * rename global tool code * rename api * fix some bugs (#5841) * fix some bugs * fix * perf: Tag filter * fix: ts * fix: ts --------- Co-authored-by: archer <545436317@qq.com> * perf: Concat function * fix: workflow snapshot push * fix: ts type * fix: login to config/* * fix: ts * fix: model avatar (#5848) * fix: model avatar * fix: ts * fix: avatar migration to s3 * update lock * fix: avatar redirect --------- Co-authored-by: archer <545436317@qq.com> * fix tool detail (#5847) * fix tool detail * init script * fix build * perf: plugin detail modal * change tooltags to tags * fix icon --------- Co-authored-by: archer <545436317@qq.com> * fix tag filter scroll (#5852) * fix create app plugin & import info (#5853) * tag size * rename toolkit * download url * import plugin status (#5854) * init doc * fix: init shell --------- Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com> Co-authored-by: Zeng Qingwen <143274079+fishwww-ww@users.noreply.github.com> Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> Co-authored-by: heheer <zhiyu44@qq.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
419 lines
14 KiB
TypeScript
419 lines
14 KiB
TypeScript
import { countGptMessagesTokens } from '../../../common/string/tiktoken/index';
|
||
import type {
|
||
ChatCompletionAssistantMessageParam,
|
||
ChatCompletionContentPart,
|
||
ChatCompletionContentPartRefusal,
|
||
ChatCompletionContentPartText,
|
||
ChatCompletionMessageParam,
|
||
SdkChatCompletionMessageParam
|
||
} from '@fastgpt/global/core/ai/type.d';
|
||
import axios from 'axios';
|
||
import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants';
|
||
import { i18nT } from '../../../../web/i18n/utils';
|
||
import { addLog } from '../../../common/system/log';
|
||
import { getImageBase64 } from '../../../common/file/image/utils';
|
||
import { getS3ChatSource } from '../../../common/s3/sources/chat';
|
||
import { isInternalAddress } from '../../../common/system/utils';
|
||
|
||
export const filterGPTMessageByMaxContext = async ({
|
||
messages = [],
|
||
maxContext
|
||
}: {
|
||
messages: ChatCompletionMessageParam[];
|
||
maxContext: number;
|
||
}) => {
|
||
if (!Array.isArray(messages)) {
|
||
return [];
|
||
}
|
||
|
||
// If the text length is less than half of the maximum token, no calculation is required
|
||
if (messages.length < 4) {
|
||
return 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);
|
||
|
||
if (chatPrompts.length === 0) {
|
||
return systemPrompts;
|
||
}
|
||
|
||
// reduce token of systemPrompt
|
||
maxContext -= await countGptMessagesTokens(systemPrompts);
|
||
|
||
/* 截取时候保证一轮内容的完整性
|
||
1. user - assistant - user
|
||
2. user - assistant - tool
|
||
3. user - assistant - tool - tool - tool
|
||
3. user - assistant - tool - assistant - tool
|
||
4. user - assistant - assistant - tool - tool
|
||
*/
|
||
// Save the last chat prompt(question)
|
||
let chats: ChatCompletionMessageParam[] = [];
|
||
let tmpChats: ChatCompletionMessageParam[] = [];
|
||
|
||
// 从后往前截取对话内容, 每次到 user 则认为是一组完整信息
|
||
while (chatPrompts.length > 0) {
|
||
const lastMessage = chatPrompts.pop();
|
||
if (!lastMessage) {
|
||
break;
|
||
}
|
||
|
||
// 遇到 user,说明到了一轮完整信息,可以开始判断是否需要保留
|
||
if (lastMessage.role === ChatCompletionRequestMessageRoleEnum.User) {
|
||
const tokens = await countGptMessagesTokens([lastMessage, ...tmpChats]);
|
||
maxContext -= tokens;
|
||
// 该轮信息整体 tokens 超出范围,这段数据不要了。但是至少保证一组。
|
||
if (maxContext < 0 && chats.length > 0) {
|
||
break;
|
||
}
|
||
|
||
chats = [lastMessage, ...tmpChats].concat(chats);
|
||
tmpChats = [];
|
||
} else {
|
||
tmpChats.unshift(lastMessage);
|
||
}
|
||
}
|
||
|
||
return [...systemPrompts, ...chats];
|
||
};
|
||
|
||
/*
|
||
Format requested messages
|
||
1. If not useVision, only retain text.
|
||
2. Remove file_url
|
||
3. If useVision, parse url from question, and load image from url(Local url)
|
||
*/
|
||
export const loadRequestMessages = async ({
|
||
messages,
|
||
useVision = false,
|
||
origin
|
||
}: {
|
||
messages: ChatCompletionMessageParam[];
|
||
useVision?: boolean;
|
||
origin?: string;
|
||
}) => {
|
||
const parseSystemMessage = (
|
||
content: string | ChatCompletionContentPartText[]
|
||
): string | ChatCompletionContentPartText[] | undefined => {
|
||
if (typeof content === 'string') {
|
||
if (!content) return;
|
||
return content;
|
||
}
|
||
|
||
const arrayContent = content
|
||
.filter((item) => item.text)
|
||
.map((item) => item.text)
|
||
.join('\n\n');
|
||
|
||
return arrayContent;
|
||
};
|
||
// Parse user content(text and img) Store history => api messages
|
||
const parseUserContent = async (content: string | ChatCompletionContentPart[]) => {
|
||
// Split question text and image
|
||
const parseStringWithImages = (input: string): ChatCompletionContentPart[] => {
|
||
if (!useVision || input.length > 500) {
|
||
return [{ type: 'text', text: input }];
|
||
}
|
||
|
||
// 正则表达式匹配图片URL
|
||
const imageRegex =
|
||
/(https?:\/\/[^\s/$.?#].[^\s]*\.(?:png|jpe?g|gif|webp|bmp|tiff?|svg|ico|heic|avif))/gi;
|
||
|
||
const result: ChatCompletionContentPart[] = [];
|
||
|
||
// 提取所有HTTPS图片URL并添加到result开头
|
||
const httpsImages = [...new Set(Array.from(input.matchAll(imageRegex), (m) => m[0]))];
|
||
httpsImages.forEach((url) => {
|
||
result.push({
|
||
type: 'image_url',
|
||
image_url: {
|
||
url: url
|
||
}
|
||
});
|
||
});
|
||
|
||
// Too many images return text
|
||
if (httpsImages.length > 4) {
|
||
return [{ type: 'text', text: input }];
|
||
}
|
||
|
||
// 添加原始input作为文本
|
||
result.push({ type: 'text', text: input });
|
||
return result;
|
||
};
|
||
// Load image to base64
|
||
const loadUserContentImage = async (content: ChatCompletionContentPart[]) => {
|
||
return Promise.all(
|
||
content.map(async (item) => {
|
||
if (item.type === 'image_url') {
|
||
// Remove url origin
|
||
const imgUrl = item.image_url.url;
|
||
|
||
// base64 image
|
||
if (imgUrl.startsWith('data:image/')) {
|
||
return item;
|
||
}
|
||
|
||
try {
|
||
// If imgUrl is a local path, load image from local, and set url to base64
|
||
if (
|
||
imgUrl.startsWith('/') ||
|
||
process.env.MULTIPLE_DATA_TO_BASE64 === 'true' ||
|
||
isInternalAddress(imgUrl)
|
||
) {
|
||
const url = await (async () => {
|
||
if (item.key) {
|
||
try {
|
||
return await getS3ChatSource().createGetChatFileURL({
|
||
key: item.key,
|
||
external: false
|
||
});
|
||
} catch (error) {}
|
||
}
|
||
return imgUrl;
|
||
})();
|
||
const { completeBase64: base64 } = await getImageBase64(url);
|
||
|
||
return {
|
||
...item,
|
||
image_url: {
|
||
...item.image_url,
|
||
url: base64
|
||
}
|
||
};
|
||
}
|
||
|
||
// 检查下这个图片是否可以被访问,如果不行的话,则过滤掉
|
||
const response = await axios.head(imgUrl, {
|
||
timeout: 10000
|
||
});
|
||
if (response.status < 200 || response.status >= 400) {
|
||
addLog.info(`Filter invalid image: ${imgUrl}`);
|
||
return;
|
||
}
|
||
} catch (error: any) {
|
||
if (error?.response?.status === 405 || error?.response?.status === 403) {
|
||
return item;
|
||
}
|
||
addLog.warn(`Filter invalid image: ${imgUrl}`, { error });
|
||
return;
|
||
}
|
||
}
|
||
return item;
|
||
})
|
||
).then((res) => res.filter(Boolean) as ChatCompletionContentPart[]);
|
||
};
|
||
|
||
if (content === undefined) return;
|
||
if (typeof content === 'string') {
|
||
if (content === '') return;
|
||
|
||
const loadImageContent = await loadUserContentImage(parseStringWithImages(content));
|
||
if (loadImageContent.length === 0) return;
|
||
return loadImageContent;
|
||
}
|
||
|
||
const result = (
|
||
await Promise.all(
|
||
content.map(async (item) => {
|
||
if (item.type === 'text') {
|
||
// If it is array, not need to parse image
|
||
if (item.text) return item;
|
||
return;
|
||
}
|
||
if (item.type === 'file_url') return; // LLM not support file_url
|
||
if (item.type === 'image_url') {
|
||
// close vision, remove image_url
|
||
if (!useVision) return;
|
||
// remove empty image_url
|
||
if (!item.image_url.url) return;
|
||
}
|
||
|
||
return item;
|
||
})
|
||
)
|
||
)
|
||
.flat()
|
||
.filter(Boolean) as ChatCompletionContentPart[];
|
||
|
||
const loadImageContent = await loadUserContentImage(result);
|
||
|
||
if (loadImageContent.length === 0) return;
|
||
return loadImageContent;
|
||
};
|
||
|
||
const formatAssistantItem = (item: ChatCompletionAssistantMessageParam) => {
|
||
return {
|
||
role: item.role,
|
||
content: item.content,
|
||
function_call: item.function_call,
|
||
name: item.name,
|
||
refusal: item.refusal,
|
||
tool_calls: item.tool_calls
|
||
};
|
||
};
|
||
const parseAssistantContent = (
|
||
content:
|
||
| string
|
||
| (ChatCompletionContentPartText | ChatCompletionContentPartRefusal)[]
|
||
| null
|
||
| undefined
|
||
) => {
|
||
if (typeof content === 'string') {
|
||
return content || '';
|
||
}
|
||
// 交互节点
|
||
if (!content) return '';
|
||
|
||
const result = content.filter((item) => item?.type === 'text');
|
||
if (result.length === 0) return '';
|
||
|
||
return result.map((item) => item.text).join('\n');
|
||
};
|
||
|
||
if (messages.length === 0) {
|
||
return Promise.reject(i18nT('common:core.chat.error.Messages empty'));
|
||
}
|
||
|
||
// 合并相邻 role 的内容,只保留一个 role, content 变成数组。 assistant 的话,工具调用不合并。
|
||
const mergeMessages = ((messages: ChatCompletionMessageParam[]): ChatCompletionMessageParam[] => {
|
||
return messages.reduce((mergedMessages: ChatCompletionMessageParam[], currentMessage) => {
|
||
const lastMessage = mergedMessages[mergedMessages.length - 1];
|
||
|
||
if (!lastMessage) {
|
||
return [currentMessage];
|
||
}
|
||
|
||
if (
|
||
lastMessage.role === ChatCompletionRequestMessageRoleEnum.System &&
|
||
currentMessage.role === ChatCompletionRequestMessageRoleEnum.System
|
||
) {
|
||
const lastContent: ChatCompletionContentPartText[] = Array.isArray(lastMessage.content)
|
||
? lastMessage.content
|
||
: [{ type: 'text', text: lastMessage.content || '' }];
|
||
const currentContent: ChatCompletionContentPartText[] = Array.isArray(
|
||
currentMessage.content
|
||
)
|
||
? currentMessage.content
|
||
: [{ type: 'text', text: currentMessage.content || '' }];
|
||
lastMessage.content = [...lastContent, ...currentContent];
|
||
} // Handle user messages
|
||
else if (
|
||
lastMessage.role === ChatCompletionRequestMessageRoleEnum.User &&
|
||
currentMessage.role === ChatCompletionRequestMessageRoleEnum.User
|
||
) {
|
||
const lastContent: ChatCompletionContentPart[] = Array.isArray(lastMessage.content)
|
||
? lastMessage.content
|
||
: [{ type: 'text', text: lastMessage.content }];
|
||
const currentContent: ChatCompletionContentPart[] = Array.isArray(currentMessage.content)
|
||
? currentMessage.content
|
||
: [{ type: 'text', text: currentMessage.content }];
|
||
lastMessage.content = [...lastContent, ...currentContent];
|
||
} else if (
|
||
lastMessage.role === ChatCompletionRequestMessageRoleEnum.Assistant &&
|
||
currentMessage.role === ChatCompletionRequestMessageRoleEnum.Assistant
|
||
) {
|
||
// Content 不为空的对象,或者是交互节点
|
||
if (
|
||
(typeof lastMessage.content === 'string' ||
|
||
Array.isArray(lastMessage.content) ||
|
||
lastMessage.interactive) &&
|
||
(typeof currentMessage.content === 'string' ||
|
||
Array.isArray(currentMessage.content) ||
|
||
currentMessage.interactive)
|
||
) {
|
||
const lastContent: (ChatCompletionContentPartText | ChatCompletionContentPartRefusal)[] =
|
||
Array.isArray(lastMessage.content)
|
||
? lastMessage.content
|
||
: [{ type: 'text', text: lastMessage.content || '' }];
|
||
const currentContent: (
|
||
| ChatCompletionContentPartText
|
||
| ChatCompletionContentPartRefusal
|
||
)[] = Array.isArray(currentMessage.content)
|
||
? currentMessage.content
|
||
: [{ type: 'text', text: currentMessage.content || '' }];
|
||
|
||
lastMessage.content = [...lastContent, ...currentContent];
|
||
} else {
|
||
// 有其中一个没有 content,说明不是连续的文本输出
|
||
mergedMessages.push(currentMessage);
|
||
}
|
||
} else {
|
||
mergedMessages.push(currentMessage);
|
||
}
|
||
|
||
return mergedMessages;
|
||
}, []);
|
||
})(messages);
|
||
|
||
const loadMessages = (
|
||
await Promise.all(
|
||
mergeMessages.map(async (item, i) => {
|
||
if (item.role === ChatCompletionRequestMessageRoleEnum.System) {
|
||
const content = parseSystemMessage(item.content);
|
||
if (!content) return;
|
||
return {
|
||
...item,
|
||
content
|
||
};
|
||
} else if (item.role === ChatCompletionRequestMessageRoleEnum.User) {
|
||
const content = await parseUserContent(item.content);
|
||
if (!content) {
|
||
return {
|
||
...item,
|
||
content: 'null'
|
||
};
|
||
}
|
||
|
||
const formatContent = (() => {
|
||
if (Array.isArray(content) && content.length === 1 && content[0].type === 'text') {
|
||
return content[0].text;
|
||
}
|
||
return content;
|
||
})();
|
||
|
||
return {
|
||
...item,
|
||
content: formatContent
|
||
};
|
||
} else if (item.role === ChatCompletionRequestMessageRoleEnum.Assistant) {
|
||
if (item.tool_calls || item.function_call) {
|
||
return formatAssistantItem(item);
|
||
}
|
||
|
||
const parseContent = parseAssistantContent(item.content);
|
||
|
||
// 如果内容为空,且前后不再是 assistant,需要补充成 null,避免丢失 user-assistant 的交互
|
||
const formatContent = (() => {
|
||
const lastItem = mergeMessages[i - 1];
|
||
const nextItem = mergeMessages[i + 1];
|
||
if (
|
||
parseContent === '' &&
|
||
(lastItem?.role === ChatCompletionRequestMessageRoleEnum.Assistant ||
|
||
nextItem?.role === ChatCompletionRequestMessageRoleEnum.Assistant)
|
||
) {
|
||
return;
|
||
}
|
||
return parseContent || 'null';
|
||
})();
|
||
if (!formatContent) return;
|
||
|
||
return {
|
||
...formatAssistantItem(item),
|
||
content: formatContent
|
||
};
|
||
} else {
|
||
return item;
|
||
}
|
||
})
|
||
)
|
||
).filter(Boolean) as ChatCompletionMessageParam[];
|
||
|
||
return loadMessages as SdkChatCompletionMessageParam[];
|
||
};
|