diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index c705cf79..aa5852b2 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -41,7 +41,6 @@ export interface IChatMessage { memoryType?: string sessionId?: string createdDate: Date - feedbackId?: string } export interface IChatMessageFeedback { @@ -49,6 +48,7 @@ export interface IChatMessageFeedback { content?: string chatflowid: string chatId: string + messageId: string rating: ChatMessageRatingType createdDate: Date } diff --git a/packages/server/src/database/entities/ChatMessage.ts b/packages/server/src/database/entities/ChatMessage.ts index 55a1c0e5..4054a26d 100644 --- a/packages/server/src/database/entities/ChatMessage.ts +++ b/packages/server/src/database/entities/ChatMessage.ts @@ -40,7 +40,4 @@ export class ChatMessage implements IChatMessage { @CreateDateColumn() createdDate: Date - - @Column({ nullable: true }) - feedbackId?: string } diff --git a/packages/server/src/database/entities/ChatMessageFeedback.ts b/packages/server/src/database/entities/ChatMessageFeedback.ts index 972994f9..811f3104 100644 --- a/packages/server/src/database/entities/ChatMessageFeedback.ts +++ b/packages/server/src/database/entities/ChatMessageFeedback.ts @@ -1,8 +1,9 @@ /* eslint-disable */ -import { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, Index } from 'typeorm' -import { ChatMessageRatingType, IChatMessageFeedback } from '../../Interface' +import { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, Index, Unique } from 'typeorm' +import { IChatMessageFeedback, ChatMessageRatingType } from '../../Interface' @Entity() +@Unique(['messageId']) export class ChatMessageFeedback implements IChatMessageFeedback { @PrimaryGeneratedColumn('uuid') id: string @@ -11,15 +12,19 @@ export class ChatMessageFeedback implements IChatMessageFeedback { @Column() chatflowid: string - @Column({ type: 'text' }) - content?: string - + @Index() @Column() chatId: string @Column() + messageId: string + + @Column({ nullable: true }) rating: ChatMessageRatingType + @Column({ nullable: true, type: 'text' }) + content?: string + @CreateDateColumn() createdDate: Date } diff --git a/packages/server/src/database/migrations/sqlite/1707213619308-AddFeedback.ts b/packages/server/src/database/migrations/sqlite/1707213619308-AddFeedback.ts index 29ac76bc..b9002697 100644 --- a/packages/server/src/database/migrations/sqlite/1707213619308-AddFeedback.ts +++ b/packages/server/src/database/migrations/sqlite/1707213619308-AddFeedback.ts @@ -3,8 +3,10 @@ import { MigrationInterface, QueryRunner } from 'typeorm' export class AddFeedback1707213619308 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `CREATE TABLE IF NOT EXISTS "chat_message_feedback" ("id" varchar PRIMARY KEY NOT NULL, "chatflowid" varchar NOT NULL, "content" text, "chatId" varchar NOT NULL, "rating" varchar NOT NULL, "createdDate" datetime NOT NULL DEFAULT (datetime('now')));` + `CREATE TABLE IF NOT EXISTS "chat_message_feedback" ("id" varchar PRIMARY KEY NOT NULL, "chatflowid" varchar NOT NULL, "chatId" varchar NOT NULL, "messageId" varchar NOT NULL, "rating" varchar NOT NULL, "content" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')));` ) + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_e574527322272fd838f4f0f3d3" ON "chat_message_feedback" ("chatflowid") ;`) + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_e574527322272fd838f4f0f3d3" ON "chat_message_feedback" ("chatId") ;`) } public async down(queryRunner: QueryRunner): Promise { diff --git a/packages/server/src/database/migrations/sqlite/1707986407818-AddFeedbackToChatMessage.ts b/packages/server/src/database/migrations/sqlite/1707986407818-AddFeedbackToChatMessage.ts deleted file mode 100644 index 24af4ada..00000000 --- a/packages/server/src/database/migrations/sqlite/1707986407818-AddFeedbackToChatMessage.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class AddFeedbackToChatMessage1707986407818 implements MigrationInterface { - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "temp_chat_message" ("id" varchar PRIMARY KEY NOT NULL, "role" varchar NOT NULL, "chatflowid" varchar NOT NULL, "content" text NOT NULL, "sourceDocuments" text, "usedTools" text, "fileAnnotations" text, "createdDate" datetime NOT NULL DEFAULT (datetime('now')), "chatType" VARCHAR NOT NULL DEFAULT 'INTERNAL', "chatId" VARCHAR NOT NULL, "memoryType" VARCHAR, "sessionId" VARCHAR, "feedbackId" varchar);` - ) - await queryRunner.query( - `INSERT INTO "temp_chat_message" ("id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "fileAnnotations", "createdDate", "chatType", "chatId", "memoryType", "sessionId") SELECT "id", "role", "chatflowid", "content", "sourceDocuments", "usedTools", "fileAnnotations", "createdDate", "chatType", "chatId", "memoryType", "sessionId" FROM "chat_message";` - ) - await queryRunner.query(`DROP TABLE "chat_message";`) - await queryRunner.query(`ALTER TABLE "temp_chat_message" RENAME TO "chat_message";`) - await queryRunner.query(`CREATE INDEX "IDX_e574527322272fd838f4f0f3d3" ON "chat_message" ("chatflowid") ;`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE IF EXISTS "temp_chat_message";`) - await queryRunner.query(`ALTER TABLE "chat_message" DROP COLUMN "feedbackId";`) - } -} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 5a54a5cd..4e6dc72c 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -12,7 +12,6 @@ import { AddCategoryToChatFlow1699900910291 } from './1699900910291-AddCategoryT import { AddFileAnnotationsToChatMessage1700271021237 } from './1700271021237-AddFileAnnotationsToChatMessage' import { AddVariableEntity1699325775451 } from './1702200925471-AddVariableEntity' import { AddFeedback1707213619308 } from './1707213619308-AddFeedback' -import { AddFeedbackToChatMessage1707986407818 } from './1707986407818-AddFeedbackToChatMessage' export const sqliteMigrations = [ Init1693835579790, @@ -28,6 +27,5 @@ export const sqliteMigrations = [ AddCategoryToChatFlow1699900910291, AddFileAnnotationsToChatMessage1700271021237, AddVariableEntity1699325775451, - AddFeedback1707213619308, - AddFeedbackToChatMessage1707986407818 + AddFeedback1707213619308 ] diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 498b2e2c..ab998a15 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -10,7 +10,7 @@ import logger from './utils/logger' import { expressRequestLogger } from './utils/logger' import { v4 as uuidv4 } from 'uuid' import OpenAI from 'openai' -import { FindOptionsWhere, MoreThanOrEqual, LessThanOrEqual } from 'typeorm' +import { FindOptionsWhere, MoreThanOrEqual, LessThanOrEqual, IsNull, Between } from 'typeorm' import { IChatFlow, IncomingInput, @@ -22,7 +22,8 @@ import { IChatMessage, IChatMessageFeedback, IDepthQueue, - INodeDirectedGraph + INodeDirectedGraph, + ChatMessageRatingType } from './Interface' import { getNodeModulesPackagePath, @@ -170,7 +171,8 @@ export class App { '/api/v1/components-credentials-icon/', '/api/v1/chatflows-streaming', '/api/v1/openai-assistants-file', - '/api/v1/ip' + '/api/v1/ip', + '/api/v1/feedback' ] this.app.use((req, res, next) => { if (req.url.includes('/api/v1/')) { @@ -610,31 +612,62 @@ export class App { // Chat Message Feedback // ---------------------------------------- - // Create new feedback + // Get all chatmessage feedback from chatflowid + this.app.get('/api/v1/feedback/:id', async (req: Request, res: Response) => { + const chatflowid = req.params.id + const chatId = req.query?.chatId as string | undefined + const sortOrder = req.query?.order as string | undefined + const startDate = req.query?.startDate as string | undefined + const endDate = req.query?.endDate as string | undefined + + const feedback = await this.getChatMessageFeedback(chatflowid, chatId, sortOrder, startDate, endDate) + + return res.json(feedback) + }) + + // Add chatmessage feedback for chatflowid this.app.post('/api/v1/feedback/:id', async (req: Request, res: Response) => { const body = req.body const results = await this.addChatMessageFeedback(body) return res.json(results) }) - // Update feedback + // Update chatmessage feedback for id this.app.put('/api/v1/feedback/:id', async (req: Request, res: Response) => { + const id = req.params.id const body = req.body - const chatMessageFeedback = await this.AppDataSource.getRepository(ChatMessageFeedback).findOneBy({ - id: req.params.id + await this.updateChatMessageFeedback(id, body) + return res.json({ status: 'OK' }) + }) + + // ---------------------------------------- + // stats + // ---------------------------------------- + // + // get stats for showing in chatflow + this.app.get('/api/v1/stats/:id', async (req: Request, res: Response) => { + const chatflowid = req.params.id + const chatTypeFilter = chatType.EXTERNAL + + const totalMessages = await this.AppDataSource.getRepository(ChatMessage).count({ + where: { + chatflowid, + chatType: chatTypeFilter + } }) - if (!chatMessageFeedback) { - res.status(404).send(`Feedback ${req.params.id} not found`) - return + const chatMessageFeedbackRepo = this.AppDataSource.getRepository(ChatMessageFeedback) + + const totalFeedback = await chatMessageFeedbackRepo.count() + const positiveFeedback = await chatMessageFeedbackRepo.countBy({ rating: ChatMessageRatingType.THUMBS_UP }) + + const results = { + totalMessages, + totalFeedback, + positiveFeedback } - const newChatMessageFeedback = new ChatMessageFeedback() - Object.assign(newChatMessageFeedback, body) - - this.AppDataSource.getRepository(ChatMessageFeedback).merge(chatMessageFeedback, newChatMessageFeedback) - const results = await this.AppDataSource.getRepository(ChatMessageFeedback).save(chatMessageFeedback) - return res.json(results) + res.json(results) }) // ---------------------------------------- @@ -1497,6 +1530,28 @@ export class App { let toDate if (endDate) toDate = setDateToStartOrEndOfDay(endDate, 'end') + if (feedback) { + const messages = await this.AppDataSource.getRepository(ChatMessage) + .createQueryBuilder('chat_message') + .leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id') + .where('chat_message.chatflowid = :chatflowid', { chatflowid }) + .andWhere(chatType ? 'chat_message.chatType = :chatType' : 'TRUE', { chatType }) + .andWhere(chatId ? 'chat_message.chatId = :chatId' : 'TRUE', { chatId }) + .andWhere(memoryType ? 'chat_message.memoryType = :memoryType' : 'TRUE', { + memoryType: memoryType ?? (chatId ? IsNull() : undefined) + }) + .andWhere(sessionId ? 'chat_message.sessionId = :sessionId' : 'TRUE', { + sessionId: sessionId ?? (chatId ? IsNull() : undefined) + }) + .andWhere(fromDate && toDate ? 'chat_message.createdDate = :createdDate' : 'TRUE', { + createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined + }) + .orderBy('chat_message.createdDate', sortOrder === 'DESC' ? 'DESC' : 'ASC') + .getMany() + + return messages + } + return await this.AppDataSource.getRepository(ChatMessage).find({ where: { chatflowid, @@ -1526,39 +1581,61 @@ export class App { return await this.AppDataSource.getRepository(ChatMessage).save(chatmessage) } - async updateChatMessage(id: string, update: Partial) { - const chatMessage = await this.AppDataSource.getRepository(ChatMessage).findOneBy({ - id + /** + * Method that get chat messages. + * @param {string} chatflowid + * @param {string} sortOrder + * @param {string} chatId + * @param {string} startDate + * @param {string} endDate + */ + async getChatMessageFeedback( + chatflowid: string, + chatId?: string, + sortOrder: string = 'ASC', + startDate?: string, + endDate?: string + ): Promise { + let fromDate + if (startDate) fromDate = new Date(startDate) + + let toDate + if (endDate) toDate = new Date(endDate) + return await this.AppDataSource.getRepository(ChatMessageFeedback).find({ + where: { + chatflowid, + chatId, + createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined + }, + order: { + createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC' + } }) - - if (!chatMessage) return - - const newChatMessage = new ChatMessage() - Object.assign(newChatMessage, update) - - this.AppDataSource.getRepository(ChatMessage).merge(chatMessage, newChatMessage) - return await this.AppDataSource.getRepository(ChatMessage).save(chatMessage) } /** - * Method that adds feedback for a chat message and updates the chat message with the feedback id. + * Method that add chat message feedback. * @param {Partial} chatMessageFeedback */ - async addChatMessageFeedback(chatMessageFeedback: Partial & { messageId: string }): Promise { - const messageId = chatMessageFeedback.messageId - const newFeedback = new ChatMessageFeedback() - Object.assign(newFeedback, chatMessageFeedback) + async addChatMessageFeedback(chatMessageFeedback: Partial): Promise { + const newChatMessageFeedback = new ChatMessageFeedback() + Object.assign(newChatMessageFeedback, chatMessageFeedback) - const feedback = this.AppDataSource.getRepository(ChatMessageFeedback).create(newFeedback) - const results = await this.AppDataSource.getRepository(ChatMessageFeedback).save(feedback) - - // use the message id to update the chat message with feedback id - await this.updateChatMessage(messageId, { feedbackId: results.id }) - - return results + const feedback = this.AppDataSource.getRepository(ChatMessageFeedback).create(newChatMessageFeedback) + return await this.AppDataSource.getRepository(ChatMessageFeedback).save(feedback) } - async updateChatMessageFeedback(id: string, update: Partial) {} + /** + * Method that updates chat message feedback. + * @param {string} id + * @param {Partial} chatMessageFeedback + */ + async updateChatMessageFeedback(id: string, chatMessageFeedback: Partial) { + const newChatMessageFeedback = new ChatMessageFeedback() + Object.assign(newChatMessageFeedback, chatMessageFeedback) + + await this.AppDataSource.getRepository(ChatMessageFeedback).update({ id }, chatMessageFeedback) + } async upsertVector(req: Request, res: Response, isInternal: boolean = false) { try { @@ -1941,7 +2018,7 @@ export class App { sessionId, createdDate: userMessageDateTime } - await this.addChatMessage(userMessage) + const newMessage = await this.addChatMessage(userMessage) let resultText = '' if (result.text) resultText = result.text @@ -1974,6 +2051,7 @@ export class App { // Prepare response result.chatId = chatId + if (newMessage.id && !isInternal) result.messageId = newMessage.id if (sessionId) result.sessionId = sessionId if (memoryType) result.memoryType = memoryType diff --git a/packages/ui/src/api/chatmessage.js b/packages/ui/src/api/chatmessage.js index 5f1a4bad..7b084c46 100644 --- a/packages/ui/src/api/chatmessage.js +++ b/packages/ui/src/api/chatmessage.js @@ -1,8 +1,9 @@ import client from './client' const getInternalChatmessageFromChatflow = (id) => client.get(`/internal-chatmessage/${id}`) -const getAllChatmessageFromChatflow = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'DESC', ...params } }) -const getChatmessageFromPK = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'ASC', ...params } }) +const getAllChatmessageFromChatflow = (id, params = {}) => + client.get(`/chatmessage/${id}`, { params: { order: 'DESC', feedback: true, ...params } }) +const getChatmessageFromPK = (id, params = {}) => client.get(`/chatmessage/${id}`, { params: { order: 'ASC', feedback: true, ...params } }) const deleteChatmessage = (id, params = {}) => client.delete(`/chatmessage/${id}`, { params: { ...params } }) export default { diff --git a/packages/ui/src/api/feedback.js b/packages/ui/src/api/feedback.js new file mode 100644 index 00000000..5b32ca26 --- /dev/null +++ b/packages/ui/src/api/feedback.js @@ -0,0 +1,7 @@ +import client from './client' + +const getStatsFromChatflow = (id) => client.get(`/stats/${id}`) + +export default { + getStatsFromChatflow +} diff --git a/packages/ui/src/ui-component/cards/StatsCard.js b/packages/ui/src/ui-component/cards/StatsCard.js new file mode 100644 index 00000000..69433505 --- /dev/null +++ b/packages/ui/src/ui-component/cards/StatsCard.js @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types' + +import { useSelector } from 'react-redux' +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' + +const StatsCard = ({ title, stat }) => { + const customization = useSelector((state) => state.customization) + return ( + + + + {title} + + + {stat} + + + + ) +} + +StatsCard.propTypes = { + title: PropTypes.string, + stat: PropTypes.string +} + +export default StatsCard diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js index cadd4abd..f52723b2 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.js @@ -37,12 +37,15 @@ import { CodeBlock } from 'ui-component/markdown/CodeBlock' import SourceDocDialog from 'ui-component/dialog/SourceDocDialog' import { MultiDropdown } from 'ui-component/dropdown/MultiDropdown' import { StyledButton } from 'ui-component/button/StyledButton' +import StatsCard from 'ui-component/cards/StatsCard' +import Feedback from 'ui-component/extended/Feedback' // store import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' // API import chatmessageApi from 'api/chatmessage' +import feedbackApi from 'api/feedback' import useApi from 'hooks/useApi' import useConfirm from 'hooks/useConfirm' @@ -83,6 +86,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const [chatlogs, setChatLogs] = useState([]) const [allChatlogs, setAllChatLogs] = useState([]) const [chatMessages, setChatMessages] = useState([]) + const [stats, setStats] = useState([]) const [selectedMessageIndex, setSelectedMessageIndex] = useState(0) const [sourceDialogOpen, setSourceDialogOpen] = useState(false) const [sourceDialogProps, setSourceDialogProps] = useState({}) @@ -92,6 +96,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow) const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK) + const getStatsApi = useApi(feedbackApi.getStatsFromChatflow) const onStartDateSelected = (date) => { setStartDate(date) @@ -366,9 +371,16 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [getChatmessageApi.data]) + useEffect(() => { + if (getStatsApi.data) { + setStats(getStatsApi.data) + } + }, [getStatsApi.data]) + useEffect(() => { if (dialogProps.chatflow) { getChatmessageApi.request(dialogProps.chatflow.id) + getStatsApi.request(dialogProps.chatflow.id) } return () => { @@ -410,7 +422,33 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { <> -
+
+ + + +
+
From Date { })}
)} + {message.type === 'apiMessage' && message.feedback ? ( + + ) : null}
) diff --git a/packages/ui/src/ui-component/extended/Feedback.js b/packages/ui/src/ui-component/extended/Feedback.js new file mode 100644 index 00000000..5a943ec5 --- /dev/null +++ b/packages/ui/src/ui-component/extended/Feedback.js @@ -0,0 +1,71 @@ +import { Alert, IconButton } from '@mui/material' +import { useTheme } from '@mui/material/styles' +import PropTypes from 'prop-types' + +const ThumbsUpIcon = () => { + return ( + + + + + ) +} + +const ThumbsDownIcon = () => { + return ( + + + + + ) +} + +const Feedback = ({ content, rating }) => { + const theme = useTheme() + + return ( +
+ {content ? ( + : } + severity={rating === 'THUMBS_UP' ? 'success' : 'error'} + style={{ marginBottom: 14 }} + variant='outlined' + > + {content ? {content} : null} + + ) : ( + + {rating === 'THUMBS_UP' ? : } + + )} +
+ ) +} + +Feedback.propTypes = { + rating: PropTypes.oneOf(['THUMBS_UP', 'THUMBS_DOWN']), + content: PropTypes.string +} + +export default Feedback