Test parse cite and add tool call parallel (#4737)

* add quote response filter (#4727)

* chatting

* add quote response filter

* add test

* remove comment

* perf: cite hidden

* perf: format llm response

* feat: comment

* update default chunk size

* update default chunk size

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2025-04-30 17:43:50 +08:00
committed by GitHub
parent 683ab6c17d
commit fdd4e9edbd
53 changed files with 1131 additions and 716 deletions

View File

@@ -21,16 +21,16 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import { getCollectionSourceData } from '@fastgpt/global/core/dataset/collection/utils';
import Markdown from '.';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import { Types } from 'mongoose';
const A = ({ children, chatAuthData, ...props }: any) => {
const A = ({ children, chatAuthData, showAnimation, ...props }: any) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const content = useMemo(() => String(children), [children]);
// empty href link
if (!props.href && typeof children?.[0] === 'string') {
const text = useMemo(() => String(children), [children]);
return (
<MyTooltip label={t('common:core.chat.markdown.Quick Question')}>
<Button
@@ -38,16 +38,23 @@ const A = ({ children, chatAuthData, ...props }: any) => {
size={'xs'}
borderRadius={'md'}
my={1}
onClick={() => eventBus.emit(EventNameEnum.sendQuestion, { text })}
onClick={() => eventBus.emit(EventNameEnum.sendQuestion, { text: content })}
>
{text}
{content}
</Button>
</MyTooltip>
);
}
// Quote
if (props.href?.startsWith('QUOTE') && typeof children?.[0] === 'string') {
// Cite
if (
(props.href?.startsWith('CITE') || props.href?.startsWith('QUOTE')) &&
typeof children?.[0] === 'string'
) {
if (!Types.ObjectId.isValid(content)) {
return <></>;
}
const {
data: quoteData,
loading,
@@ -74,6 +81,7 @@ const A = ({ children, chatAuthData, ...props }: any) => {
onClose={onClose}
onOpen={() => {
onOpen();
if (showAnimation) return;
getQuoteDataById(String(children));
}}
trigger={'hover'}
@@ -90,7 +98,7 @@ const A = ({ children, chatAuthData, ...props }: any) => {
</Button>
</PopoverTrigger>
<PopoverContent boxShadow={'lg'} w={'500px'} maxW={'90vw'} py={4}>
<MyBox isLoading={loading}>
<MyBox isLoading={loading || showAnimation}>
<PopoverArrow />
<PopoverBody py={0} px={0} fontSize={'sm'}>
<Flex px={4} pb={1} justifyContent={'space-between'}>

View File

@@ -60,9 +60,9 @@ const MarkdownRender = ({
img: Image,
pre: RewritePre,
code: Code,
a: (props: any) => <A {...props} chatAuthData={chatAuthData} />
a: (props: any) => <A {...props} showAnimation={showAnimation} chatAuthData={chatAuthData} />
};
}, [chatAuthData]);
}, [chatAuthData, showAnimation]);
const formatSource = useMemo(() => {
if (showAnimation || forbidZhFormat) return source;

View File

@@ -27,14 +27,14 @@ export const mdTextFormat = (text: string) => {
return match;
});
// 处理 [quote:id] 格式引用,将 [quote:675934a198f46329dfc6d05a] 转换为 [675934a198f46329dfc6d05a](QUOTE)
// 处理 [quote:id] 格式引用,将 [quote:675934a198f46329dfc6d05a] 转换为 [675934a198f46329dfc6d05a](CITE)
text = text
// .replace(
// /([\u4e00-\u9fa5\u3000-\u303f])([a-zA-Z0-9])|([a-zA-Z0-9])([\u4e00-\u9fa5\u3000-\u303f])/g,
// '$1$3 $2$4'
// )
// 处理 格式引用,将 [675934a198f46329dfc6d05a] 转换为 [675934a198f46329dfc6d05a](QUOTE)
.replace(/\[([a-f0-9]{24})\](?!\()/g, '[$1](QUOTE)');
// 处理 格式引用,将 [675934a198f46329dfc6d05a] 转换为 [675934a198f46329dfc6d05a](CITE)
.replace(/\[([a-f0-9]{24})\](?!\()/g, '[$1](CITE)');
// 处理链接后的中文标点符号,增加空格
text = text.replace(/(https?:\/\/[^\s、]+)([,。!?;:、])/g, '$1 $2');

View File

@@ -240,11 +240,6 @@ const ChatItem = (props: Props) => {
quoteId?: string;
}) => {
if (!setQuoteData) return;
if (isChatting)
return toast({
title: t('chat:chat.waiting_for_response'),
status: 'info'
});
const collectionIdList = collectionId
? [collectionId]
@@ -277,18 +272,7 @@ const ChatItem = (props: Props) => {
}
});
},
[
setQuoteData,
isChatting,
toast,
t,
quoteList,
isShowReadRawSource,
appId,
chatId,
chat.dataId,
outLinkAuthData
]
[setQuoteData, quoteList, isShowReadRawSource, appId, chatId, chat.dataId, outLinkAuthData]
);
useEffect(() => {

View File

@@ -96,8 +96,6 @@ const RenderText = React.memo(function RenderText({
text: string;
chatItemDataId: string;
}) {
const isResponseDetail = useContextSelector(ChatItemContext, (v) => v.isResponseDetail);
const appId = useContextSelector(ChatBoxContext, (v) => v.appId);
const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId);
const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData);
@@ -106,10 +104,8 @@ const RenderText = React.memo(function RenderText({
if (!text) return '';
// Remove quote references if not showing response detail
return isResponseDetail
? text
: text.replace(/\[([a-f0-9]{24})\]\(QUOTE\)/g, '').replace(/\[([a-f0-9]{24})\](?!\()/g, '');
}, [text, isResponseDetail]);
return text;
}, [text]);
const chatAuthData = useCreation(() => {
return { appId, chatId, chatItemDataId, ...outLinkAuthData };

View File

@@ -12,7 +12,7 @@ import { getWebLLMModel } from '@/web/common/system/utils';
const SearchParamsTip = ({
searchMode,
similarity = 0,
limit = 1500,
limit = 5000,
responseEmptyText,
usingReRank = false,
datasetSearchUsingExtensionQuery,

View File

@@ -5,8 +5,8 @@ import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updatePasswordByOld } from '@/web/support/user/api';
import { checkPasswordRule } from '@/web/support/user/login/constants';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { checkPasswordRule } from '@fastgpt/global/common/string/password';
type FormType = {
oldPsw: string;

View File

@@ -1,7 +1,7 @@
import React, { Dispatch } from 'react';
import { FormControl, Box, Input, Button } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum, checkPasswordRule } from '@/web/support/user/login/constants';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { postFindPassword } from '@/web/support/user/api';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import type { ResLogin } from '@/global/support/api/userRes.d';
@@ -9,6 +9,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { checkPasswordRule } from '@fastgpt/global/common/string/password';
interface Props {
setPageType: Dispatch<`${LoginPageTypeEnum}`>;

View File

@@ -1,7 +1,7 @@
import React, { Dispatch } from 'react';
import { FormControl, Box, Input, Button } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum, checkPasswordRule } from '@/web/support/user/login/constants';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { postRegister } from '@/web/support/user/api';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import type { ResLogin } from '@/global/support/api/userRes';
@@ -19,6 +19,7 @@ import {
getSourceDomain,
removeFastGPTSem
} from '@/web/support/marketing/utils';
import { checkPasswordRule } from '@fastgpt/global/common/string/password';
interface Props {
loginSuccess: (e: ResLogin) => void;

View File

@@ -16,7 +16,7 @@ import { reRankRecall } from '@fastgpt/service/core/ai/rerank';
import { aiTranscriptions } from '@fastgpt/service/core/ai/audio/transcriptions';
import { isProduction } from '@fastgpt/global/common/system/constants';
import * as fs from 'fs';
import { llmCompletionsBodyFormat, llmResponseToAnswerText } from '@fastgpt/service/core/ai/utils';
import { llmCompletionsBodyFormat, formatLLMResponse } from '@fastgpt/service/core/ai/utils';
export type testQuery = { model: string; channelId?: number };
@@ -78,7 +78,7 @@ const testLLMModel = async (model: LLMModelItemType, headers: Record<string, str
model
);
const { response, isStreamResponse } = await createChatCompletion({
const { response } = await createChatCompletion({
modelData: model,
body: requestBody,
options: {
@@ -88,7 +88,7 @@ const testLLMModel = async (model: LLMModelItemType, headers: Record<string, str
}
}
});
const { text: answer } = await llmResponseToAnswerText(response);
const { text: answer } = await formatLLMResponse(response);
if (answer) {
return answer;

View File

@@ -9,7 +9,10 @@ import { authChatCrud } from '@/service/support/permission/auth/chat';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import {
filterPublicNodeResponseData,
removeAIResponseCite
} from '@fastgpt/global/core/chat/utils';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { ChatItemType } from '@fastgpt/global/core/chat/type';
@@ -83,6 +86,13 @@ async function handler(
}
});
}
if (!responseDetail) {
histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) {
item.value = removeAIResponseCite(item.value, false);
}
});
}
return {
list: isPlugin ? histories : transformPreviewHistories(histories, responseDetail),

View File

@@ -19,7 +19,7 @@ async function handler(req: ApiRequestProps<SearchTestProps>): Promise<SearchTes
const {
datasetId,
text,
limit = 1500,
limit = 5000,
similarity,
searchMode,
embeddingWeight,

View File

@@ -30,6 +30,7 @@ import {
concatHistories,
filterPublicNodeResponseData,
getChatTitleFromChatMessage,
removeAIResponseCite,
removeEmptyUserInput
} from '@fastgpt/global/core/chat/utils';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
@@ -74,7 +75,7 @@ export type Props = ChatCompletionCreateParams &
responseChatItemId?: string;
stream?: boolean;
detail?: boolean;
parseQuote?: boolean;
retainDatasetCite?: boolean;
variables: Record<string, any>; // Global variables or plugin inputs
};
@@ -107,7 +108,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
stream = false,
detail = false,
parseQuote = false,
retainDatasetCite = false,
messages = [],
variables = {},
responseChatItemId = getNanoid(),
@@ -187,6 +188,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
chatId
});
})();
retainDatasetCite = retainDatasetCite && !!responseDetail;
const isPlugin = app.type === AppTypeEnum.plugin;
// Check message type
@@ -291,7 +293,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
chatConfig,
histories: newHistories,
stream,
parseQuote,
retainDatasetCite,
maxRunTimes: WORKFLOW_MAX_RUN_TIMES,
workflowStreamResponse: workflowResponseWrite
});
@@ -406,17 +408,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return assistantResponses;
})();
const formatResponseContent = removeAIResponseCite(responseContent, retainDatasetCite);
const error = flowResponses[flowResponses.length - 1]?.error;
res.json({
...(detail ? { responseData: feResponseData, newVariables } : {}),
error,
id: chatId || '',
id: saveChatId,
model: '',
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 1 },
choices: [
{
message: { role: 'assistant', content: responseContent },
message: { role: 'assistant', content: formatResponseContent },
finish_reason: 'stop',
index: 0
}

View File

@@ -30,6 +30,7 @@ import {
concatHistories,
filterPublicNodeResponseData,
getChatTitleFromChatMessage,
removeAIResponseCite,
removeEmptyUserInput
} from '@fastgpt/global/core/chat/utils';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
@@ -74,7 +75,7 @@ export type Props = ChatCompletionCreateParams &
responseChatItemId?: string;
stream?: boolean;
detail?: boolean;
parseQuote?: boolean;
retainDatasetCite?: boolean;
variables: Record<string, any>; // Global variables or plugin inputs
};
@@ -107,7 +108,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
stream = false,
detail = false,
parseQuote = false,
retainDatasetCite = false,
messages = [],
variables = {},
responseChatItemId = getNanoid(),
@@ -187,6 +188,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
chatId
});
})();
retainDatasetCite = retainDatasetCite && !!responseDetail;
const isPlugin = app.type === AppTypeEnum.plugin;
// Check message type
@@ -290,7 +292,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
chatConfig,
histories: newHistories,
stream,
parseQuote,
retainDatasetCite,
maxRunTimes: WORKFLOW_MAX_RUN_TIMES,
workflowStreamResponse: workflowResponseWrite,
version: 'v2',
@@ -401,6 +403,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return assistantResponses;
})();
const formatResponseContent = removeAIResponseCite(responseContent, retainDatasetCite);
const error = flowResponses[flowResponses.length - 1]?.error;
res.json({
@@ -411,7 +414,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 1 },
choices: [
{
message: { role: 'assistant', content: responseContent },
message: { role: 'assistant', content: formatResponseContent },
finish_reason: 'stop',
index: 0
}

View File

@@ -87,6 +87,7 @@ const OutLink = (props: Props) => {
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const isResponseDetail = useContextSelector(ChatItemContext, (v) => v.isResponseDetail);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
@@ -162,7 +163,8 @@ const OutLink = (props: Props) => {
},
responseChatItemId,
chatId: completionChatId,
...outLinkAuthData
...outLinkAuthData,
retainDatasetCite: isResponseDetail
},
onMessage: generatingMessage,
abortCtrl: controller
@@ -200,6 +202,7 @@ const OutLink = (props: Props) => {
chatId,
customVariables,
outLinkAuthData,
isResponseDetail,
onUpdateHistoryTitle,
setChatBoxData,
forbidLoadChat,

View File

@@ -17,7 +17,7 @@ import {
} from '@fastgpt/service/common/string/tiktoken/index';
import { pushDataListToTrainingQueueByCollectionId } from '@fastgpt/service/core/dataset/training/controller';
import { loadRequestMessages } from '@fastgpt/service/core/chat/utils';
import { llmCompletionsBodyFormat, llmResponseToAnswerText } from '@fastgpt/service/core/ai/utils';
import { llmCompletionsBodyFormat, formatLLMResponse } from '@fastgpt/service/core/ai/utils';
import { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import {
chunkAutoChunkSize,
@@ -140,7 +140,7 @@ ${replaceVariable(Prompt_AgentQA.fixedText, { text })}`;
modelData
)
});
const { text: answer, usage } = await llmResponseToAnswerText(chatResponse);
const { text: answer, usage } = await formatLLMResponse(chatResponse);
const inputTokens = usage?.prompt_tokens || (await countGptMessagesTokens(messages));
const outputTokens = usage?.completion_tokens || (await countPromptTokens(answer));

View File

@@ -37,6 +37,7 @@ import { saveChat } from '@fastgpt/service/core/chat/saveChat';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { removeDatasetCiteText } from '@fastgpt/service/core/ai/utils';
export const pluginNodes2InputSchema = (
nodes: { flowNodeType: FlowNodeTypeEnum; inputs: FlowNodeInputItemType[] }[]
@@ -288,7 +289,7 @@ export const callMcpServerTool = async ({ key, toolName, inputs }: toolCallProps
})();
// Format response content
responseContent = responseContent.trim().replace(/\[\w+\]\(QUOTE\)/g, '');
responseContent = removeDatasetCiteText(responseContent.trim(), false);
return responseContent;
};

View File

@@ -132,7 +132,7 @@ export const streamFetch = ({
variables,
detail: true,
stream: true,
parseQuote: true
retainDatasetCite: data.retainDatasetCite ?? true
})
};

View File

@@ -4,22 +4,3 @@ export enum LoginPageTypeEnum {
forgetPassword = 'forgetPassword',
wechat = 'wechat'
}
export const checkPasswordRule = (password: string) => {
const patterns = [
/\d/, // Contains digits
/[a-z]/, // Contains lowercase letters
/[A-Z]/, // Contains uppercase letters
/[!@#$%^&*()_+=-]/ // Contains special characters
];
const validChars = /^[\dA-Za-z!@#$%^&*()_+=-]{6,100}$/;
// Check length and valid characters
if (!validChars.test(password)) return false;
// Count how many patterns are satisfied
const matchCount = patterns.filter((pattern) => pattern.test(password)).length;
// Must satisfy at least 2 patterns
return matchCount >= 2;
};