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 <m13203533462@163.com>
This commit is contained in:
a.e.
2024-11-08 15:35:27 +08:00
committed by archer
parent 8ede7add01
commit 0a238845ab
5 changed files with 223 additions and 36 deletions

View File

@@ -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<ChatItem>,
MongoChatItem.findById(chatItemIdAi).lean() as Promise<ChatItem>
]);
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 });
});
};

View File

@@ -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 { MongoApp } from '../app/schema';
import { import {
ChatItemValueTypeEnum, ChatItemValueTypeEnum,
@@ -13,6 +18,7 @@ import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { getAppChatConfig, getGuideModule } from '@fastgpt/global/core/workflow/utils'; import { getAppChatConfig, getGuideModule } from '@fastgpt/global/core/workflow/utils';
import { AppChatConfigType } from '@fastgpt/global/core/app/type'; import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils'; import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
import { pushChatLog } from './pushChatLog';
type Props = { type Props = {
chatId: string; chatId: string;
@@ -67,7 +73,7 @@ export async function saveChat({
}); });
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
await MongoChatItem.insertMany( const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.insertMany(
content.map((item) => ({ content.map((item) => ({
chatId, chatId,
teamId, teamId,
@@ -105,6 +111,13 @@ export async function saveChat({
upsert: true upsert: true
} }
); );
pushChatLog({
chatId,
chatItemIdHuman: String(chatItemIdHuman),
chatItemIdAi: String(chatItemIdAi),
appId
});
}); });
if (isUpdateUseTime) { if (isUpdateUseTime) {

View File

@@ -46,4 +46,9 @@ STORE_LOG_LEVEL=warn
# 工作流最大运行次数,避免极端的死循环情况 # 工作流最大运行次数,避免极端的死循环情况
WORKFLOW_MAX_RUN_TIMES=500 WORKFLOW_MAX_RUN_TIMES=500
# 循环最大运行次数,避免极端的死循环情况 # 循环最大运行次数,避免极端的死循环情况
WORKFLOW_MAX_LOOP_TIMES=50 WORKFLOW_MAX_LOOP_TIMES=50
# 对话日志推送服务
# URL/INTERVAL 为空时不推送
CHAT_LOG_URL=http://localhost:8080
CHAT_LOG_INTERVAL=10000

View File

@@ -63,6 +63,8 @@ import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/u
type FastGptWebChatProps = { type FastGptWebChatProps = {
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
appId?: string; appId?: string;
customUid?: string; // non-undefined: will be the priority provider for the logger.
metadata?: Record<string, any>;
}; };
export type Props = ChatCompletionCreateParams & export type Props = ChatCompletionCreateParams &
@@ -99,6 +101,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
let { let {
chatId, chatId,
appId, appId,
customUid,
// share chat // share chat
shareId, shareId,
outLinkUid, outLinkUid,
@@ -110,7 +113,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
detail = false, detail = false,
messages = [], messages = [],
variables = {}, variables = {},
responseChatItemId = getNanoid() responseChatItemId = getNanoid(),
metadata
} = req.body as Props; } = req.body as Props;
const originIp = requestIp.getClientIp(req); const originIp = requestIp.getClientIp(req);
@@ -122,7 +126,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
throw new Error('messages is not array'); throw new Error('messages is not array');
} }
/* /*
Web params: chatId + [Human] Web params: chatId + [Human]
API params: chatId + [Human] API params: chatId + [Human]
API params: [histories, Human] API params: [histories, Human]
@@ -139,41 +143,50 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return JSON.stringify(variables); return JSON.stringify(variables);
})(); })();
/* /*
1. auth app permission 1. auth app permission
2. auth balance 2. auth balance
3. get app 3. get app
4. parse outLink token 4. parse outLink token
*/ */
const { teamId, tmbId, user, app, responseDetail, authType, apikey, canWrite, outLinkUserId } = const {
await (async () => { teamId,
// share chat tmbId,
if (shareId && outLinkUid) { user,
return authShareChat({ app,
shareId, responseDetail,
outLinkUid, authType,
chatId, apikey,
ip: originIp, canWrite,
question: startHookText outLinkUserId = customUid
}); } = await (async () => {
} // share chat
// team space chat if (shareId && outLinkUid) {
if (spaceTeamId && appId && teamToken) { return authShareChat({
return authTeamSpaceChat({ shareId,
teamId: spaceTeamId, outLinkUid,
teamToken, chatId,
appId, ip: originIp,
chatId question: startHookText
}); });
} }
// team space chat
/* parse req: api or token */ if (spaceTeamId && appId && teamToken) {
return authHeaderRequest({ return authTeamSpaceChat({
req, teamId: spaceTeamId,
teamToken,
appId, appId,
chatId chatId
}); });
})(); }
/* parse req: api or token */
return authHeaderRequest({
req,
appId,
chatId
});
})();
const isPlugin = app.type === AppTypeEnum.plugin; const isPlugin = app.type === AppTypeEnum.plugin;
// Check message type // Check message type
@@ -333,7 +346,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
source, source,
content: [userQuestion, aiResponse], content: [userQuestion, aiResponse],
metadata: { metadata: {
originIp originIp,
...metadata
} }
}); });
} }

View File

@@ -41,6 +41,7 @@ type Props = {
appAvatar: string; appAvatar: string;
shareId: string; shareId: string;
authToken: string; authToken: string;
customUid: string;
}; };
const OutLink = ({ const OutLink = ({
@@ -371,13 +372,13 @@ const OutLink = ({
}; };
const Render = (props: Props) => { const Render = (props: Props) => {
const { shareId, authToken } = props; const { shareId, authToken, customUid } = props;
const { localUId, loaded } = useShareChatStore(); const { localUId, loaded } = useShareChatStore();
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(false);
const contextParams = useMemo(() => { const contextParams = useMemo(() => {
return { shareId, outLinkUid: authToken || localUId }; return { shareId, outLinkUid: authToken || customUid || localUId };
}, [authToken, localUId, shareId]); }, [authToken, customUid, localUId, shareId]);
useMount(() => { useMount(() => {
setIsLoaded(true); setIsLoaded(true);
@@ -401,6 +402,7 @@ export default React.memo(Render);
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {
const shareId = context?.query?.shareId || ''; const shareId = context?.query?.shareId || '';
const authToken = context?.query?.authToken || ''; const authToken = context?.query?.authToken || '';
const customUid = context?.query?.customUid || '';
const app = await (async () => { const app = await (async () => {
try { try {
@@ -427,6 +429,7 @@ export async function getServerSideProps(context: any) {
appIntro: app?.appId?.intro ?? 'intro', appIntro: app?.appId?.intro ?? 'intro',
shareId: shareId ?? '', shareId: shareId ?? '',
authToken: authToken ?? '', authToken: authToken ?? '',
customUid,
...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow'])) ...(await serviceSideProps(context, ['file', 'app', 'chat', 'workflow']))
} }
}; };