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 { 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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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']))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user