Skip to content

Commit

Permalink
Merge pull request #626 from encorelab/625-feature-add-ai-assistant-c…
Browse files Browse the repository at this point in the history
…hat-log-downloads

Added download chat history button for AI Assistant
  • Loading branch information
markiianbabiak authored Oct 29, 2024
2 parents 268c27f + 7036c9d commit 02078cb
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 29 deletions.
45 changes: 45 additions & 0 deletions backend/src/api/chatHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// backend/src/api/chatHistory.ts

import { Router } from 'express';
import dalChatMessage from '../repository/dalChatMessage';

const router = Router();

router.post('/', async (req, res) => {
try {
const { boardId, userId } = req.body;
const chatHistory = await dalChatMessage.getByBoardIdAndUserId(
boardId,
userId
);

// Format the chat history as a CSV string
const csvString = formatChatHistoryAsCSV(chatHistory);

res.setHeader('Content-Type', 'text/csv');
res.setHeader(
'Content-Disposition',
'attachment; filename="chat_history.csv"'
);
res.send(csvString);
} catch (error) {
console.error('Error fetching or formatting chat history:', error);
res.status(500).json({ error: 'Failed to download chat history.' });
}
});

function formatChatHistoryAsCSV(chatHistory: any[]): string {

Check warning on line 31 in backend/src/api/chatHistory.ts

View workflow job for this annotation

GitHub Actions / Linting and Code Formating Check CI (backend)

Unexpected any. Specify a different type
// 1. Create header row
let csvString = 'Timestamp,Role,Content\n';

// 2. Add data rows
for (const message of chatHistory) {
csvString += `"${message.createdAt.toLocaleString()}","${
message.role
}","${message.content.replace(/"/g, '""')}"\n`;
}

return csvString;
}

export default router;
21 changes: 21 additions & 0 deletions backend/src/models/ChatMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { prop, getModelForClass, modelOptions } from '@typegoose/typegoose';

@modelOptions({
schemaOptions: { collection: 'chatMessages', timestamps: true },
})
export class ChatMessageModel {
@prop({ required: true })
public userId!: string;

@prop({ required: true })
public boardId!: string;

@prop({ required: true, enum: ['user', 'assistant'] })
public role!: string;

@prop({ required: true })
public content!: string;
}

const ChatMessage = getModelForClass(ChatMessageModel);
export default ChatMessage;
31 changes: 31 additions & 0 deletions backend/src/repository/dalChatMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import ChatMessage, { ChatMessageModel } from '../models/ChatMessage';

export const save = async (message: ChatMessageModel) => {
try {
const savedMessage = await ChatMessage.create(message);
return savedMessage;
} catch (err) {
throw new Error(JSON.stringify(err, null, ' '));
}
};

export const getByBoardIdAndUserId = async (
boardId: string,
userId: string
) => {
try {
const messages = await ChatMessage.find({ boardId, userId }).sort({
createdAt: 1,
}); // Sort by createdAt in descending order (oldest to most recent)
return messages;
} catch (err) {
throw new Error(JSON.stringify(err, null, ' '));
}
};

const dalChatMessage = {
save,
getByBoardIdAndUserId,
};

export default dalChatMessage;
2 changes: 2 additions & 0 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import learner from './api/learner';
import { isAuthenticated } from './utils/auth';
import RedisClient from './utils/redis';
import aiRouter from './api/ai';
import chatHistoryRouter from './api/chatHistory';
dotenv.config();

const port = process.env.PORT || 8001;
Expand Down Expand Up @@ -68,6 +69,7 @@ app.use('/api/trace', isAuthenticated, trace);
app.use('/api/todoItems', isAuthenticated, todoItems);
app.use('/api/learner', isAuthenticated, learner);
app.use('/api/ai', isAuthenticated, aiRouter);
app.use('/api/chat-history', chatHistoryRouter);

app.get('*', (req, res) => {
res.sendFile(path.join(staticFilesPath, 'index.html'));
Expand Down
50 changes: 37 additions & 13 deletions backend/src/services/vertexAI/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dalVote from '../../repository/dalVote';
import dalBucket from '../../repository/dalBucket';
import dalChatMessage from '../../repository/dalChatMessage';
import { BucketModel } from '../../models/Bucket';
import { generateUniqueID } from '../../utils/Utils';
import dalPost from '../../repository/dalPost';
Expand Down Expand Up @@ -341,6 +342,14 @@ function parseJsonResponse(response: string): any {
}
}

function removeEnd(str: string): string {
const endIndex = str.indexOf('<END>');
if (endIndex !== -1) {
return str.substring(0, endIndex);
}
return str; // Return original string if <END> is not found
}

async function sendMessage(
posts: any[],
prompt: string,
Expand All @@ -350,6 +359,17 @@ async function sendMessage(
// Send an initial acknowledgment
socket.emit(SocketEvent.AI_RESPONSE, { status: 'Received' });

const userId = socket.data.userId;
const boardId = socket.data.boardId;

// Save user prompt
await dalChatMessage.save({
userId: userId,
boardId: boardId,
role: 'user',
content: prompt
});

// 1. Fetch Upvote Counts and Create Map
const upvoteMap = await fetchUpvoteCounts(posts);

Expand All @@ -364,7 +384,7 @@ async function sendMessage(
);

// 4. Fetch and Format Buckets
const bucketsToSend = await fetchAndFormatBuckets(posts);
const bucketsToSend = await fetchAndFormatBuckets(boardId);

// 5. Construct and Send Message to LLM (streaming)
constructAndSendMessage(postsWithBucketIds, bucketsToSend, prompt).then(
Expand Down Expand Up @@ -419,6 +439,15 @@ async function sendMessage(
status: 'Completed',
response: finalResponse.response,
});

// Save AI response
await dalChatMessage.save({
userId: userId,
boardId: boardId,
role: 'assistant',
content: removeEnd(finalResponse.response)
});

} else {
const errorMessage = `Completed with invalid formatting: ${partialResponse}`;
socket.emit(SocketEvent.AI_RESPONSE, {
Expand Down Expand Up @@ -476,7 +505,7 @@ async function sendMessage(
async function performDatabaseOperations(
response: any,
posts: any[],
socket: socketIO.Socket
socket: socketIO.Socket,
) {
const validPostIds = new Set(posts.map((post) => post.postID));

Expand All @@ -486,7 +515,7 @@ async function performDatabaseOperations(

const newBucket: BucketModel = {
bucketID: generateUniqueID(),
boardID: posts[0].boardID,
boardID: socket.data.boardId,
name: bucketName,
posts: [],
};
Expand All @@ -505,7 +534,7 @@ async function performDatabaseOperations(

const newBucket: BucketModel = {
bucketID: generateUniqueID(),
boardID: posts[0].boardID,
boardID: socket.data.boardId,
name: name,
posts: [],
};
Expand Down Expand Up @@ -545,7 +574,7 @@ async function performDatabaseOperations(
postID
);
} else if (name) {
const bucket = await dalBucket.getByName(name, posts[0].boardID);
const bucket = await dalBucket.getByName(name, socket.data.boardId);
if (bucket) {
actionsByBucket[bucket.bucketID] = (
actionsByBucket[bucket.bucketID] || []
Expand Down Expand Up @@ -591,7 +620,7 @@ async function performDatabaseOperations(
postID
);
} else if (name) {
const bucket = await dalBucket.getByName(name, posts[0].boardID);
const bucket = await dalBucket.getByName(name, socket.data.boardId);
if (bucket) {
actionsByBucket[bucket.bucketID] = (
actionsByBucket[bucket.bucketID] || []
Expand Down Expand Up @@ -724,14 +753,9 @@ async function constructAndSendMessage(
}
}

async function fetchAndFormatBuckets(posts: any[]): Promise<any[]> {
// Return empty list if no posts
if (!posts || posts.length === 0) {
return [];
}

async function fetchAndFormatBuckets(boardId: string): Promise<any[]> {
// Fetch ALL buckets for the board
const buckets = await dalBucket.getByBoardId(posts[0].boardID); // Assuming all posts belong to the same board
const buckets = await dalBucket.getByBoardId(boardId); // Assuming all posts belong to the same board

// Create a list of buckets with their bucketIDs
return buckets.map((bucket) => ({
Expand Down
16 changes: 9 additions & 7 deletions backend/src/socket/events/ai.events.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
// ai.events.ts
import { Server, Socket } from 'socket.io';
import { SocketEvent } from '../../constants';
import { sendMessage } from '../../services/vertexAI';
import { SocketPayload } from '../types/event.types'; // Import the type for SocketPayload

// Define a type for the AI message data
interface AiMessageData {
boardID: string;
posts: any[]; // Or a more specific type for your posts
prompt: string;
boardId: string;
userId: string;
}

class AiMessage {
static type: SocketEvent = SocketEvent.AI_MESSAGE;

static async handleEvent(
data: SocketPayload<AiMessageData>
): Promise<{ posts: any[]; prompt: string }> {
const { posts, prompt } = data.eventData;
): Promise<{ posts: any[]; prompt: string; boardId: string; userId: string }> {
const { posts, prompt, boardId, userId } = data.eventData;
// ... any necessary data processing or validation ...
return { posts, prompt };
return { posts, prompt, boardId, userId };
}

static async handleResult(
io: Server,
socket: Socket,
result: { posts: any[]; prompt: string; boardID: string }
result: { posts: any[]; prompt: string; boardId: string; userId: string }
): Promise<void> {
const { posts, prompt } = result;
const { posts, prompt, boardId, userId } = result;
socket.data.boardId = boardId;
socket.data.userId = userId;
sendMessage(posts, prompt, socket);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,8 +618,11 @@ <h3 class="message-label">Assistant:</h3>
<mat-label>Ask the AI Assistant</mat-label>
<input matInput [(ngModel)]="aiPrompt" (keyup.enter)="askAI()" #aiInput />
</mat-form-field>
<button mat-raised-button color="primary" (click)="askAI()" *ngIf="selected.value === 3">Send</button>

<div style="display: flex; gap: 10px;">
<button mat-raised-button color="primary" (click)="askAI()" *ngIf="selected.value === 3">Send</button>
<button mat-raised-button color="primary" (click)="downloadChatHistory()" *ngIf="selected.value === 3">Download Chat History</button>
</div>

<button mat-button (click)="onNoClick()">Close</button>
<button
mat-button
Expand Down
Loading

0 comments on commit 02078cb

Please sign in to comment.