From 0a238845ab4c9e7965d82f05c2a0cfb277a1dce1 Mon Sep 17 00:00:00 2001 From: "a.e." <49438478+I-Info@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:35:27 +0800 Subject: [PATCH] feat: support push chat log (#3093) * feat: custom uid/metadata * to: custom info * fix: chat push latest * feat: add chat log envs * refactor: move timer to pushChatLog * fix: using precise log --------- Co-authored-by: Finley Ge --- packages/service/core/chat/pushChatLog.ts | 152 ++++++++++++++++++ packages/service/core/chat/saveChat.ts | 17 +- projects/app/.env.template | 7 +- .../app/src/pages/api/v1/chat/completions.ts | 74 +++++---- projects/app/src/pages/chat/share.tsx | 9 +- 5 files changed, 223 insertions(+), 36 deletions(-) create mode 100644 packages/service/core/chat/pushChatLog.ts diff --git a/packages/service/core/chat/pushChatLog.ts b/packages/service/core/chat/pushChatLog.ts new file mode 100644 index 000000000..91d31e357 --- /dev/null +++ b/packages/service/core/chat/pushChatLog.ts @@ -0,0 +1,152 @@ +import { addLog } from '../../common/system/log'; +import { MongoChatItem } from './chatItemSchema'; +import { MongoChat } from './chatSchema'; +import axios from 'axios'; +import { ChatItemType } from '@fastgpt/global/core/chat/type'; + +export type Metadata = { + [key: string]: { + label: string; + value: string; + }; +}; + +export const pushChatLog = ({ + chatId, + chatItemIdHuman, + chatItemIdAi, + appId, + metadata +}: { + chatId: string; + chatItemIdHuman: string; + chatItemIdAi: string; + appId: string; + metadata?: Metadata; +}) => { + const interval = Number(process.env.CHAT_LOG_INTERVAL); + const url = process.env.CHAT_LOG_URL; + if (interval > 0 && url) { + addLog.info(`[ChatLogPush] push chat log after ${interval}ms`, { + appId, + chatItemIdHuman, + chatItemIdAi + }); + setTimeout(() => { + pushChatLogInternal({ chatId, chatItemIdHuman, chatItemIdAi, appId, url, metadata }); + }, interval); + } +}; + +type ChatItem = ChatItemType & { + userGoodFeedback?: string; + userBadFeedback?: string; + chatId: string; + responseData: { + moduleType: string; + runningTime: number; //s + historyPreview: { obj: string; value: string }[]; + }[]; + time: Date; +}; + +type ChatLog = { + title: string; + feedback: 'like' | 'dislike' | null; + chatItemId: string; + uid: string; + question: string; + answer: string; + chatId: string; + responseTime: number; + metadata: string; + sourceName: string; + createdAt: number; + sourceId: string; +}; + +const pushChatLogInternal = async ({ + chatId, + chatItemIdHuman, + chatItemIdAi, + appId, + url, + metadata +}: { + chatId: string; + chatItemIdHuman: string; + chatItemIdAi: string; + appId: string; + url: string; + metadata?: Metadata; +}) => { + const [chatItemHuman, chatItemAi] = await Promise.all([ + MongoChatItem.findById(chatItemIdHuman).lean() as Promise, + MongoChatItem.findById(chatItemIdAi).lean() as Promise + ]); + const [chat] = (await MongoChat.find({ chatId }).lean()) as { + title: string; + outLinkUid: string | undefined; + tmbId: string; + teamId: string; + metadata: Object; + source: string; + }[]; + + // addLog.warn('ChatLogDebug', chat); + // addLog.warn('ChatLogDebug', { chatItemHuman, chatItemAi }); + + if (!chat) { + return; + } + + const metadataString = JSON.stringify(metadata ?? {}); + + const uid = chat.outLinkUid || chat.tmbId; + // Pop last two items + const question = chatItemHuman.value[chatItemHuman.value.length - 1]?.text?.content; + const answer = chatItemAi.value[chatItemAi.value.length - 1]?.text?.content; + if (!question || !answer) { + addLog.error('[ChatLogPush] question or answer is empty', { + question: chatItemHuman.value, + answer: chatItemAi.value + }); + return; + } + const responseData = chatItemAi.responseData; + let responseTime = 0; + responseData.forEach((item) => { + responseTime += item.runningTime; + }); + + const chatLog: ChatLog = { + title: chat.title, + feedback: (() => { + if (chatItemAi.userGoodFeedback) { + return 'like'; + } else if (chatItemAi.userBadFeedback) { + return 'dislike'; + } else { + return null; + } + })(), + chatItemId: `${chatItemIdHuman},${chatItemIdAi}`, + uid, + question, + answer, + chatId, + responseTime: responseTime * 1000, + metadata: metadataString, + sourceName: chat.source ?? '-', + createdAt: new Date(chatItemAi.time).getTime(), + sourceId: `crbeer-fastgpt-${appId}` + }; + await axios + .post(url + '/api/chat/push', chatLog) + .then((res) => { + addLog.info('[ChatLogPush] push success', res.data); + }) + .catch((e) => { + addLog.error('[ChatLogPush] push failed', { e, resData: e.response?.data }); + }); +}; diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index 5614b94cc..cfadd6099 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -1,4 +1,9 @@ -import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type.d'; +import type { + AIChatItemType, + ChatItemType, + UserChatItemType +} from '@fastgpt/global/core/chat/type.d'; +import axios from 'axios'; import { MongoApp } from '../app/schema'; import { ChatItemValueTypeEnum, @@ -13,6 +18,7 @@ import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { getAppChatConfig, getGuideModule } from '@fastgpt/global/core/workflow/utils'; import { AppChatConfigType } from '@fastgpt/global/core/app/type'; import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils'; +import { pushChatLog } from './pushChatLog'; type Props = { chatId: string; @@ -67,7 +73,7 @@ export async function saveChat({ }); await mongoSessionRun(async (session) => { - await MongoChatItem.insertMany( + const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.insertMany( content.map((item) => ({ chatId, teamId, @@ -105,6 +111,13 @@ export async function saveChat({ upsert: true } ); + + pushChatLog({ + chatId, + chatItemIdHuman: String(chatItemIdHuman), + chatItemIdAi: String(chatItemIdAi), + appId + }); }); if (isUpdateUseTime) { diff --git a/projects/app/.env.template b/projects/app/.env.template index 69cd81a85..f6721c42c 100644 --- a/projects/app/.env.template +++ b/projects/app/.env.template @@ -46,4 +46,9 @@ STORE_LOG_LEVEL=warn # 工作流最大运行次数,避免极端的死循环情况 WORKFLOW_MAX_RUN_TIMES=500 # 循环最大运行次数,避免极端的死循环情况 -WORKFLOW_MAX_LOOP_TIMES=50 \ No newline at end of file +WORKFLOW_MAX_LOOP_TIMES=50 + +# 对话日志推送服务 +# URL/INTERVAL 为空时不推送 +CHAT_LOG_URL=http://localhost:8080 +CHAT_LOG_INTERVAL=10000 diff --git a/projects/app/src/pages/api/v1/chat/completions.ts b/projects/app/src/pages/api/v1/chat/completions.ts index 54efb006b..1526f5b6e 100644 --- a/projects/app/src/pages/api/v1/chat/completions.ts +++ b/projects/app/src/pages/api/v1/chat/completions.ts @@ -63,6 +63,8 @@ import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/u type FastGptWebChatProps = { chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db appId?: string; + customUid?: string; // non-undefined: will be the priority provider for the logger. + metadata?: Record; }; export type Props = ChatCompletionCreateParams & @@ -99,6 +101,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { let { chatId, appId, + customUid, // share chat shareId, outLinkUid, @@ -110,7 +113,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { detail = false, messages = [], variables = {}, - responseChatItemId = getNanoid() + responseChatItemId = getNanoid(), + metadata } = req.body as Props; const originIp = requestIp.getClientIp(req); @@ -122,7 +126,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { throw new Error('messages is not array'); } - /* + /* Web params: chatId + [Human] API params: chatId + [Human] API params: [histories, Human] @@ -139,41 +143,50 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return JSON.stringify(variables); })(); - /* + /* 1. auth app permission 2. auth balance 3. get app 4. parse outLink token */ - const { teamId, tmbId, user, app, responseDetail, authType, apikey, canWrite, outLinkUserId } = - await (async () => { - // share chat - if (shareId && outLinkUid) { - return authShareChat({ - shareId, - outLinkUid, - chatId, - ip: originIp, - question: startHookText - }); - } - // team space chat - if (spaceTeamId && appId && teamToken) { - return authTeamSpaceChat({ - teamId: spaceTeamId, - teamToken, - appId, - chatId - }); - } - - /* parse req: api or token */ - return authHeaderRequest({ - req, + const { + teamId, + tmbId, + user, + app, + responseDetail, + authType, + apikey, + canWrite, + outLinkUserId = customUid + } = await (async () => { + // share chat + if (shareId && outLinkUid) { + return authShareChat({ + shareId, + outLinkUid, + chatId, + ip: originIp, + question: startHookText + }); + } + // team space chat + if (spaceTeamId && appId && teamToken) { + return authTeamSpaceChat({ + teamId: spaceTeamId, + teamToken, appId, chatId }); - })(); + } + + /* parse req: api or token */ + return authHeaderRequest({ + req, + appId, + chatId + }); + })(); const isPlugin = app.type === AppTypeEnum.plugin; // Check message type @@ -333,7 +346,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { source, content: [userQuestion, aiResponse], metadata: { - originIp + originIp, + ...metadata } }); } diff --git a/projects/app/src/pages/chat/share.tsx b/projects/app/src/pages/chat/share.tsx index adc7f1349..70a9a80e5 100644 --- a/projects/app/src/pages/chat/share.tsx +++ b/projects/app/src/pages/chat/share.tsx @@ -41,6 +41,7 @@ type Props = { appAvatar: string; shareId: string; authToken: string; + customUid: string; }; const OutLink = ({ @@ -371,13 +372,13 @@ const OutLink = ({ }; const Render = (props: Props) => { - const { shareId, authToken } = props; + const { shareId, authToken, customUid } = props; const { localUId, loaded } = useShareChatStore(); const [isLoaded, setIsLoaded] = useState(false); const contextParams = useMemo(() => { - return { shareId, outLinkUid: authToken || localUId }; - }, [authToken, localUId, shareId]); + return { shareId, outLinkUid: authToken || customUid || localUId }; + }, [authToken, customUid, localUId, shareId]); useMount(() => { setIsLoaded(true); @@ -401,6 +402,7 @@ export default React.memo(Render); export async function getServerSideProps(context: any) { const shareId = context?.query?.shareId || ''; const authToken = context?.query?.authToken || ''; + const customUid = context?.query?.customUid || ''; const app = await (async () => { try { @@ -427,6 +429,7 @@ export async function getServerSideProps(context: any) { appIntro: app?.appId?.intro ?? 'intro', shareId: shareId ?? '', authToken: authToken ?? '', + customUid, ...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow'])) } };