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:
152
packages/service/core/chat/pushChatLog.ts
Normal file
152
packages/service/core/chat/pushChatLog.ts
Normal 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 });
|
||||
});
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -47,3 +47,8 @@ STORE_LOG_LEVEL=warn
|
||||
WORKFLOW_MAX_RUN_TIMES=500
|
||||
# 循环最大运行次数,避免极端的死循环情况
|
||||
WORKFLOW_MAX_LOOP_TIMES=50
|
||||
|
||||
# 对话日志推送服务
|
||||
# URL/INTERVAL 为空时不推送
|
||||
CHAT_LOG_URL=http://localhost:8080
|
||||
CHAT_LOG_INTERVAL=10000
|
||||
|
||||
@@ -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<string, any>;
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -145,8 +149,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
3. get app
|
||||
4. parse outLink token
|
||||
*/
|
||||
const { teamId, tmbId, user, app, responseDetail, authType, apikey, canWrite, outLinkUserId } =
|
||||
await (async () => {
|
||||
const {
|
||||
teamId,
|
||||
tmbId,
|
||||
user,
|
||||
app,
|
||||
responseDetail,
|
||||
authType,
|
||||
apikey,
|
||||
canWrite,
|
||||
outLinkUserId = customUid
|
||||
} = await (async () => {
|
||||
// share chat
|
||||
if (shareId && outLinkUid) {
|
||||
return authShareChat({
|
||||
@@ -333,7 +346,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
source,
|
||||
content: [userQuestion, aiResponse],
|
||||
metadata: {
|
||||
originIp
|
||||
originIp,
|
||||
...metadata
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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']))
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user