chatbot : step 1

This commit is contained in:
morteza-mortezai
2025-11-29 12:25:13 +03:30
parent 3ebae01605
commit db266a1cd6
18 changed files with 689 additions and 765 deletions
+223 -216
View File
@@ -1,80 +1,86 @@
import { EntityManager } from "@mikro-orm/postgresql";
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { inject, injectable } from "inversify";
import { isValidObjectId } from "mongoose";
import { LangChainService } from "./langchain.service";
import { LLMService } from "./llm.service";
import { User } from "../../users/entities/user.entity";
// import { UsersService } from "../../users/services/users.service";
import { UserModel } from "../../user/models/user.model";
import { CreateChatSessionDto } from "../DTO/create-chat-session.dto";
import { SendMessageDto } from "../DTO/send-message.dto";
import { ChatMessage, MessageStatus, MessageType } from "../entities/chat-message.entity";
import { ChatSession, ChatSessionStatus } from "../entities/chat-session.entity";
import { ChatMessageModel, MessageStatus, MessageType } from "../models/chat-message.model";
import { ChatSessionModel, ChatSessionStatus } from "../models/chat-session.model";
import { IChatContext } from "../interfaces/chatbot.interface";
import { IChatMessage } from "../models/Abstraction/IChatMessage";
import { IChatSession } from "../models/Abstraction/IChatSession";
import { ChatMessageRepository } from "../repositories/chat-message.repository";
import { ChatSessionRepository } from "../repositories/chat-session.repository";
import { BadRequestError, NotFoundError } from "../../../core/app/app.errors";
import { Logger } from "../../../core/logging/logger";
import { IOCTYPES } from "../../../IOC/ioc.types";
export enum LLMProvider {
LANGCHAIN = "langchain",
GEMINI = "gemini",
}
@Injectable()
@injectable()
export class ChatbotService {
private readonly logger = new Logger(ChatbotService.name);
private readonly logger: Logger;
private defaultProvider: LLMProvider;
constructor(
private em: EntityManager,
private llmService: LLMService,
private langChainService: LangChainService,
private chatSessionRepo: ChatSessionRepository,
private chatMessageRepo: ChatMessageRepository,
private configService: ConfigService,
// private usersService: UsersService,
@inject(IOCTYPES.ChatbotLLMService) private llmService: LLMService,
@inject(IOCTYPES.ChatbotLangChainService) private langChainService: LangChainService,
@inject(IOCTYPES.ChatSessionRepository) private chatSessionRepo: ChatSessionRepository,
@inject(IOCTYPES.ChatMessageRepository) private chatMessageRepo: ChatMessageRepository,
) {
this.logger = new Logger("ChatbotService");
// Configure default provider - LangChain for enhanced responses
this.defaultProvider = this.configService.get("DEFAULT_LLM_PROVIDER") === "gemini" ? LLMProvider.GEMINI : LLMProvider.LANGCHAIN;
this.logger.log(`Using ${this.defaultProvider} as default LLM provider`);
this.defaultProvider = process.env.DEFAULT_LLM_PROVIDER === "gemini" ? LLMProvider.GEMINI : LLMProvider.LANGCHAIN;
this.logger.info(`Using ${this.defaultProvider} as default LLM provider`);
}
async createChatSession(userId: string, createDto: CreateChatSessionDto) {
const em = this.em.fork();
const user = await em.findOne(User, { id: userId });
if (!user) {
throw new NotFoundException("User not found");
if (!isValidObjectId(userId)) {
throw new BadRequestError("Invalid user ID");
}
const session = new ChatSession();
session.title = createDto.title;
session.user = user;
session.context = createDto.context || {};
session.lastMessageAt = new Date();
const user = await UserModel.findById(userId);
if (!user) {
throw new NotFoundError("User not found");
}
await em.persistAndFlush(session);
const session = await ChatSessionModel.create({
title: createDto.title,
user: userId,
context: createDto.context || {},
lastMessageAt: new Date(),
status: ChatSessionStatus.ACTIVE,
});
return this.mapSessionToDto(session);
}
//************************************ */
async getUserChatSessions(userId: string, limit = 10) {
const em = this.em.fork();
const sessions = await this.chatSessionRepo.findByUserWithMessages(userId, limit, em);
if (!isValidObjectId(userId)) {
throw new BadRequestError("Invalid user ID");
}
const sessions = await this.chatSessionRepo.findByUserWithMessages(userId, limit);
return sessions.map((session) => this.mapSessionToDto(session));
}
//************************************ */
async getChatSession(sessionId: string, userId: string) {
const em = this.em.fork();
const session = await em.findOne(ChatSession, { id: sessionId, user: userId }, { populate: ["messages", "messages.sender"] });
if (!isValidObjectId(sessionId) || !isValidObjectId(userId)) {
throw new BadRequestError("Invalid session or user ID");
}
const session = await this.chatSessionRepo.findByUserAndId(sessionId, userId);
if (!session) {
throw new NotFoundException("Chat session not found");
throw new NotFoundError("Chat session not found");
}
return this.mapSessionToDto(session);
}
//************************************ */
async sendMessage(userId: string, sendDto: SendMessageDto, provider?: LLMProvider) {
const selectedProvider = provider || this.defaultProvider;
@@ -93,47 +99,44 @@ export class ChatbotService {
}
}
//************************************ */
private async sendMessageWithGemini(userId: string, sendDto: SendMessageDto) {
const em = this.em.fork();
const session = await em.findOne(ChatSession, { id: sendDto.sessionId, user: userId }, { populate: ["messages"] });
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
throw new BadRequestError("Invalid session or user ID");
}
const session = await ChatSessionModel.findOne({ _id: sendDto.sessionId, user: userId }).populate("messages").lean();
if (!session) {
throw new NotFoundException("Chat session not found");
throw new NotFoundError("Chat session not found");
}
if (session.status !== ChatSessionStatus.ACTIVE) {
throw new BadRequestException("Cannot send message to inactive session");
throw new BadRequestError("Cannot send message to inactive session");
}
const user = await em.findOne(User, { id: userId });
const user = await UserModel.findById(userId);
if (!user) {
throw new NotFoundException("User not found");
throw new NotFoundError("User not found");
}
// Create user message
const userMessage = new ChatMessage();
userMessage.content = sendDto.content;
userMessage.type = MessageType.USER;
userMessage.session = session;
userMessage.sender = user;
userMessage.responseToId = sendDto.responseToId;
userMessage.metadata = sendDto.metadata;
await em.persistAndFlush(userMessage);
const userMessage = await ChatMessageModel.create({
content: sendDto.content,
type: MessageType.USER,
session: sendDto.sessionId,
sender: userId,
responseToId: sendDto.responseToId,
metadata: sendDto.metadata,
});
// Update session last message time
await this.chatSessionRepo.updateLastMessageTime(session.id, em);
await this.chatSessionRepo.updateLastMessageTime(sendDto.sessionId);
// Generate bot response asynchronously using original Gemini service
this.generateBotResponse(session, userMessage, userId);
this.generateBotResponse(sendDto.sessionId, userMessage._id.toString(), userId);
return this.mapMessageToDto(userMessage);
}
//************************************ */
async sendMessageStream(userId: string, sendDto: SendMessageDto, provider?: LLMProvider) {
const selectedProvider = provider || this.defaultProvider;
@@ -151,46 +154,45 @@ export class ChatbotService {
}
}
//************************************ */
private async sendMessageStreamWithGemini(userId: string, sendDto: SendMessageDto) {
const em = this.em.fork();
const session = await em.findOne(ChatSession, { id: sendDto.sessionId, user: userId }, { populate: ["messages"] });
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
throw new BadRequestError("Invalid session or user ID");
}
const session = await ChatSessionModel.findOne({ _id: sendDto.sessionId, user: userId }).lean();
if (!session) {
throw new NotFoundException("Chat session not found");
throw new NotFoundError("Chat session not found");
}
if (session.status !== ChatSessionStatus.ACTIVE) {
throw new BadRequestException("Cannot send message to inactive session");
throw new BadRequestError("Cannot send message to inactive session");
}
const user = await em.findOne(User, { id: userId });
const user = await UserModel.findById(userId);
if (!user) {
throw new NotFoundException("User not found");
throw new NotFoundError("User not found");
}
// Create user message
const userMessage = new ChatMessage();
userMessage.content = sendDto.content;
userMessage.type = MessageType.USER;
userMessage.session = session;
userMessage.sender = user;
userMessage.responseToId = sendDto.responseToId;
userMessage.metadata = sendDto.metadata;
await em.persistAndFlush(userMessage);
const userMessage = await ChatMessageModel.create({
content: sendDto.content,
type: MessageType.USER,
session: sendDto.sessionId,
sender: userId,
responseToId: sendDto.responseToId,
metadata: sendDto.metadata,
});
// Update session last message time
await this.chatSessionRepo.updateLastMessageTime(session.id, em);
await this.chatSessionRepo.updateLastMessageTime(sendDto.sessionId);
// Get conversation history for context
const history = await this.chatMessageRepo.getConversationHistory(session.id, 20, em);
const history = await this.chatMessageRepo.getConversationHistory(sendDto.sessionId, 20);
// Build context for LLM
const context: IChatContext = {
userId,
sessionId: session.id,
sessionId: sendDto.sessionId,
conversationHistory: history.reverse().map((msg) => ({
content: msg.content,
type: msg.type as "user" | "bot" | "system",
@@ -209,18 +211,20 @@ export class ChatbotService {
};
}
//************************************ */
private async generateBotResponse(session: ChatSession, userMessage: ChatMessage, userId: string): Promise<void> {
const em = this.em.fork();
private async generateBotResponse(sessionId: string, userMessageId: string, userId: string): Promise<void> {
try {
// Get conversation history
const history = await this.chatMessageRepo.getConversationHistory(session.id, 20, em);
const history = await this.chatMessageRepo.getConversationHistory(sessionId, 20);
const session = await ChatSessionModel.findById(sessionId).lean();
if (!session) {
throw new NotFoundError("Session not found");
}
// Build context for LLM
const context: IChatContext = {
userId,
sessionId: session.id,
sessionId,
conversationHistory: history.reverse().map((msg) => ({
content: msg.content,
type: msg.type as "user" | "bot" | "system",
@@ -230,182 +234,183 @@ export class ChatbotService {
userPreferences: session.context,
};
const userMessage = await ChatMessageModel.findById(userMessageId);
if (!userMessage) {
throw new NotFoundError("User message not found");
}
// Generate response using LLM
const llmResponse = await this.llmService.generateResponse(userMessage.content, context);
// Create bot message
const botMessage = new ChatMessage();
botMessage.content = llmResponse.message;
botMessage.type = MessageType.BOT;
botMessage.session = session;
botMessage.responseToId = userMessage.id;
botMessage.tokensUsed = llmResponse.tokensUsed;
botMessage.metadata = {
confidence: llmResponse.confidence,
sources: llmResponse.sources,
llmContext: llmResponse.context,
};
await em.persistAndFlush(botMessage);
const botMessage = await ChatMessageModel.create({
content: llmResponse.message,
type: MessageType.BOT,
session: sessionId,
responseToId: userMessageId,
tokensUsed: llmResponse.tokensUsed,
metadata: {
confidence: llmResponse.confidence,
sources: llmResponse.sources,
llmContext: llmResponse.context,
},
});
// Update session last message time
await this.chatSessionRepo.updateLastMessageTime(session.id, em);
await this.chatSessionRepo.updateLastMessageTime(sessionId);
this.logger.log(`Generated bot response for session ${session.id}`);
this.logger.info(`Generated bot response for session ${sessionId}`);
} catch (error) {
this.logger.error(`Failed to generate bot response for session ${session.id}`, error);
this.logger.error(`Failed to generate bot response for session ${sessionId}`, error);
// Create error message
const errorMessage = new ChatMessage();
errorMessage.content = "متأسفم، در حال حاضر مشکلی در پاسخگویی دارم. لطفاً دوباره تلاش کنید. 🙏";
errorMessage.type = MessageType.BOT;
errorMessage.session = session;
errorMessage.responseToId = userMessage.id;
errorMessage.status = MessageStatus.FAILED;
await em.persistAndFlush(errorMessage);
await ChatMessageModel.create({
content: "متأسفم، در حال حاضر مشکلی در پاسخگویی دارم. لطفاً دوباره تلاش کنید. 🙏",
type: MessageType.BOT,
session: sessionId,
responseToId: userMessageId,
status: MessageStatus.FAILED,
});
}
}
//************************************ */
async closeChatSession(sessionId: string, userId: string) {
const em = this.em.fork();
const session = await em.findOne(ChatSession, { id: sessionId, user: userId });
if (!session) {
throw new NotFoundException("Chat session not found");
if (!isValidObjectId(sessionId) || !isValidObjectId(userId)) {
throw new BadRequestError("Invalid session or user ID");
}
session.status = ChatSessionStatus.CLOSED;
await em.persistAndFlush(session);
const session = await ChatSessionModel.findOneAndUpdate(
{ _id: sessionId, user: userId },
{ status: ChatSessionStatus.CLOSED },
{ new: true },
);
if (!session) {
throw new NotFoundError("Chat session not found");
}
return { message: "Chat session closed successfully" };
}
//************************************ */
async markMessagesAsRead(sessionId: string, userId: string, messageIds: string[]) {
const em = this.em.fork();
const session = await em.findOne(ChatSession, { id: sessionId, user: userId });
if (!session) {
throw new NotFoundException("Chat session not found");
if (!isValidObjectId(sessionId) || !isValidObjectId(userId)) {
throw new BadRequestError("Invalid session or user ID");
}
await this.chatMessageRepo.markAsRead(messageIds, em);
const session = await ChatSessionModel.findOne({ _id: sessionId, user: userId });
if (!session) {
throw new NotFoundError("Chat session not found");
}
await this.chatMessageRepo.markAsRead(messageIds);
return { message: "Messages marked as read successfully" };
}
//************************************ */
private mapSessionToDto(session: ChatSession) {
private mapSessionToDto(session: IChatSession | any) {
const sessionObj = session.toObject ? session.toObject() : session;
return {
id: session.id,
title: session.title,
status: session.status,
createdAt: session.createdAt,
lastMessageAt: session.lastMessageAt,
context: session.context,
messages: session.messages ? session.messages.getItems().map((msg) => this.mapMessageToDto(msg)) : [],
id: sessionObj._id.toString(),
title: sessionObj.title,
status: sessionObj.status,
createdAt: sessionObj.createdAt,
lastMessageAt: sessionObj.lastMessageAt,
context: sessionObj.context,
messages: sessionObj.messages ? sessionObj.messages.map((msg: any) => this.mapMessageToDto(msg)) : [],
};
}
//************************************ */
private mapMessageToDto(message: ChatMessage) {
private mapMessageToDto(message: IChatMessage | any) {
const messageObj = message.toObject ? message.toObject() : message;
return {
id: message.id,
content: message.content,
type: message.type,
status: message.status,
createdAt: message.createdAt,
responseToId: message.responseToId,
metadata: message.metadata,
tokensUsed: message.tokensUsed,
id: messageObj._id.toString(),
content: messageObj.content,
type: messageObj.type,
status: messageObj.status,
createdAt: messageObj.createdAt,
responseToId: messageObj.responseToId,
metadata: messageObj.metadata,
tokensUsed: messageObj.tokensUsed,
};
}
//************************************ */
async sendMessageWithLangChain(userId: string, sendDto: SendMessageDto) {
const em = this.em.fork();
const session = await em.findOne(ChatSession, { id: sendDto.sessionId, user: userId }, { populate: ["messages"] });
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
throw new BadRequestError("Invalid session or user ID");
}
const session = await ChatSessionModel.findOne({ _id: sendDto.sessionId, user: userId }).lean();
if (!session) {
throw new NotFoundException("Chat session not found");
throw new NotFoundError("Chat session not found");
}
if (session.status !== ChatSessionStatus.ACTIVE) {
throw new BadRequestException("Cannot send message to inactive session");
throw new BadRequestError("Cannot send message to inactive session");
}
const user = await em.findOne(User, { id: userId });
const user = await UserModel.findById(userId);
if (!user) {
throw new NotFoundException("User not found");
throw new NotFoundError("User not found");
}
// Create user message
const userMessage = new ChatMessage();
userMessage.content = sendDto.content;
userMessage.type = MessageType.USER;
userMessage.session = session;
userMessage.sender = user;
userMessage.responseToId = sendDto.responseToId;
userMessage.metadata = sendDto.metadata;
await em.persistAndFlush(userMessage);
const userMessage = await ChatMessageModel.create({
content: sendDto.content,
type: MessageType.USER,
session: sendDto.sessionId,
sender: userId,
responseToId: sendDto.responseToId,
metadata: sendDto.metadata,
});
// Update session last message time
await this.chatSessionRepo.updateLastMessageTime(session.id, em);
await this.chatSessionRepo.updateLastMessageTime(sendDto.sessionId);
// Generate bot response using LangChain
this.generateBotResponseWithLangChain(session, userMessage, userId);
this.generateBotResponseWithLangChain(sendDto.sessionId, userMessage._id.toString(), userId);
return this.mapMessageToDto(userMessage);
}
//************************************ */
async sendMessageStreamWithLangChain(userId: string, sendDto: SendMessageDto) {
const em = this.em.fork();
const session = await em.findOne(ChatSession, { id: sendDto.sessionId, user: userId }, { populate: ["messages"] });
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
throw new BadRequestError("Invalid session or user ID");
}
const session = await ChatSessionModel.findOne({ _id: sendDto.sessionId, user: userId }).lean();
if (!session) {
throw new NotFoundException("Chat session not found");
throw new NotFoundError("Chat session not found");
}
if (session.status !== ChatSessionStatus.ACTIVE) {
throw new BadRequestException("Cannot send message to inactive session");
throw new BadRequestError("Cannot send message to inactive session");
}
const user = await em.findOne(User, { id: userId });
const user = await UserModel.findById(userId);
if (!user) {
throw new NotFoundException("User not found");
throw new NotFoundError("User not found");
}
// Create user message
const userMessage = new ChatMessage();
userMessage.content = sendDto.content;
userMessage.type = MessageType.USER;
userMessage.session = session;
userMessage.sender = user;
userMessage.responseToId = sendDto.responseToId;
userMessage.metadata = sendDto.metadata;
await em.persistAndFlush(userMessage);
const userMessage = await ChatMessageModel.create({
content: sendDto.content,
type: MessageType.USER,
session: sendDto.sessionId,
sender: userId,
responseToId: sendDto.responseToId,
metadata: sendDto.metadata,
});
// Update session last message time
await this.chatSessionRepo.updateLastMessageTime(session.id, em);
await this.chatSessionRepo.updateLastMessageTime(sendDto.sessionId);
// Get conversation history for context
const history = await this.chatMessageRepo.getConversationHistory(session.id, 20, em);
const history = await this.chatMessageRepo.getConversationHistory(sendDto.sessionId, 20);
// Build context for LangChain
const context: IChatContext = {
userId,
sessionId: session.id,
sessionId: sendDto.sessionId,
conversationHistory: history.reverse().map((msg) => ({
content: msg.content,
type: msg.type as "user" | "bot" | "system",
@@ -424,18 +429,20 @@ export class ChatbotService {
};
}
//************************************ */
private async generateBotResponseWithLangChain(session: ChatSession, userMessage: ChatMessage, userId: string): Promise<void> {
const em = this.em.fork();
private async generateBotResponseWithLangChain(sessionId: string, userMessageId: string, userId: string): Promise<void> {
try {
// Get conversation history
const history = await this.chatMessageRepo.getConversationHistory(session.id, 20, em);
const history = await this.chatMessageRepo.getConversationHistory(sessionId, 20);
const session = await ChatSessionModel.findById(sessionId).lean();
if (!session) {
throw new NotFoundError("Session not found");
}
// Build context for LangChain
const context: IChatContext = {
userId,
sessionId: session.id,
sessionId,
conversationHistory: history.reverse().map((msg) => ({
content: msg.content,
type: msg.type as "user" | "bot" | "system",
@@ -445,51 +452,51 @@ export class ChatbotService {
userPreferences: session.context,
};
const userMessage = await ChatMessageModel.findById(userMessageId);
if (!userMessage) {
throw new NotFoundError("User message not found");
}
// Generate response using LangChain
const langChainResponse = await this.langChainService.generateResponse(userMessage.content, context);
// Create bot message
const botMessage = new ChatMessage();
botMessage.content = langChainResponse.message;
botMessage.type = MessageType.BOT;
botMessage.session = session;
botMessage.responseToId = userMessage.id;
botMessage.tokensUsed = langChainResponse.tokensUsed;
botMessage.metadata = {
confidence: langChainResponse.confidence,
sources: langChainResponse.sources,
llmContext: langChainResponse.context,
provider: "langchain",
documentsRetrieved: langChainResponse.context?.documentsRetrieved || 0,
};
await em.persistAndFlush(botMessage);
const botMessage = await ChatMessageModel.create({
content: langChainResponse.message,
type: MessageType.BOT,
session: sessionId,
responseToId: userMessageId,
tokensUsed: langChainResponse.tokensUsed,
metadata: {
confidence: langChainResponse.confidence,
sources: langChainResponse.sources,
llmContext: langChainResponse.context,
provider: "langchain",
documentsRetrieved: langChainResponse.context?.documentsRetrieved || 0,
},
});
// Update session last message time
await this.chatSessionRepo.updateLastMessageTime(session.id, em);
await this.chatSessionRepo.updateLastMessageTime(sessionId);
this.logger.log(`Generated LangChain bot response for session ${session.id}`);
this.logger.info(`Generated LangChain bot response for session ${sessionId}`);
} catch (error) {
this.logger.error(`Failed to generate LangChain bot response for session ${session.id}`, error);
this.logger.error(`Failed to generate LangChain bot response for session ${sessionId}`, error);
// Fallback to regular LLM service
this.logger.log(`Falling back to regular LLM service for session ${session.id}`);
await this.generateBotResponse(session, userMessage, userId);
this.logger.info(`Falling back to regular LLM service for session ${sessionId}`);
await this.generateBotResponse(sessionId, userMessageId, userId);
}
}
//************************************ */
async refreshLangChainData() {
try {
await this.langChainService.refreshVectorStore();
this.logger.log("LangChain vector store refreshed successfully");
this.logger.info("LangChain vector store refreshed successfully");
return { message: "LangChain vector store refreshed successfully" };
} catch (error) {
this.logger.error("Failed to refresh LangChain vector store", error);
throw new Error("Failed to refresh training data");
}
}
//************************************ */
}
@@ -1,22 +1,19 @@
import { EntityManager } from "@mikro-orm/core";
import { Injectable, Logger } from "@nestjs/common";
import { injectable } from "inversify";
import { Business } from "../../businesses/entities/business.entity";
import { Company } from "../../companies/entities/company.entity";
import { Industry } from "../../industries/entities/industry.entity";
import { Invoice } from "../../invoices/entities/invoice.entity";
import { InvoiceStatus } from "../../invoices/enums/invoice-status.enum";
import { Payment } from "../../payments/entities/payment.entity";
import { PaymentStatus } from "../../payments/enums/payment-status.enum";
import { Ticket } from "../../tickets/entities/ticket.entity";
import { TicketStatus } from "../../tickets/enums/ticket-status.enum";
import { IChatContext } from "../interfaces/chatbot.interface";
import { Logger } from "../../../core/logging/logger";
@Injectable()
/**
* Service for retrieving relevant context data from the database
* This is a simplified version - can be extended to fetch actual data from your database models
*/
@injectable()
export class DataContextService {
private readonly logger = new Logger(DataContextService.name);
private readonly logger: Logger;
constructor(private em: EntityManager) {}
constructor() {
this.logger = new Logger("DataContextService");
}
async getRelevantContext(message: string, context: IChatContext): Promise<{ data: string[]; sources: string[] }> {
const relevantData: string[] = [];
@@ -26,15 +23,22 @@ export class DataContextService {
// Analyze message to determine what data to fetch
const keywords = this.extractKeywords(message);
// Fetch relevant data based on keywords and user context
await Promise.all([
this.getBusinessData(keywords, context.userId, relevantData, sources),
this.getInvoiceData(keywords, context.userId, relevantData, sources),
this.getPaymentData(keywords, context.userId, relevantData, sources),
this.getCompanyData(keywords, context.userId, relevantData, sources),
this.getTicketData(keywords, context.userId, relevantData, sources),
this.getIndustryData(keywords, relevantData, sources),
]);
// TODO: Implement actual data fetching based on your project's models
// For now, this returns empty data - you can extend this to fetch:
// - Product data from ProductModel
// - Category data from CategoryModel
// - Order data from OrderModel
// - User data from UserModel
// etc.
// Example: If you want to fetch product data
// if (this.hasProductKeywords(keywords)) {
// const products = await ProductModel.find({ ... }).limit(5);
// products.forEach((product) => {
// relevantData.push(`Product: ${product.title_fa} - Price: ${product.price}`);
// sources.push("Products Database");
// });
// }
return { data: relevantData, sources };
} catch (error) {
@@ -53,174 +57,19 @@ export class DataContextService {
.filter((word) => word.length > 2 && !commonWords.includes(word));
}
private async getBusinessData(keywords: string[], userId: string, relevantData: string[], sources: string[]): Promise<void> {
if (this.hasBusinessKeywords(keywords)) {
try {
const businesses = await this.em.find(Business, { danakSubscriptionId: userId }, { limit: 5, populate: ["users"] });
businesses.forEach((business) => {
relevantData.push(
`Business: ${business.name} - Domain: ${business.domain || "No domain"} - Verified: ${business.isDomainVerified ? "Yes" : "No"}`,
);
sources.push("Businesses Database");
});
} catch (error) {
this.logger.warn("Failed to fetch business data", error);
}
}
// Helper methods for keyword detection - can be extended
private hasProductKeywords(keywords: string[]): boolean {
const productTerms = ["product", "item", "goods", "merchandise", "کالا", "محصول"];
return keywords.some((keyword) => productTerms.includes(keyword));
}
private async getInvoiceData(keywords: string[], userId: string, relevantData: string[], sources: string[]): Promise<void> {
if (this.hasInvoiceKeywords(keywords)) {
try {
// Find user's businesses first
const userBusinesses = await this.em.find(Business, { danakSubscriptionId: userId });
if (userBusinesses.length > 0) {
const businessIds = userBusinesses.map((b) => b.id);
const invoices = await this.em.find(Invoice, { business: { $in: businessIds } }, { limit: 10, orderBy: { createdAt: "DESC" } });
const summary = {
total: invoices.length,
paid: invoices.filter((inv) => inv.status === InvoiceStatus.PAID).length,
pending: invoices.filter((inv) => inv.status === InvoiceStatus.PENDING).length,
totalAmount: invoices.reduce((sum, inv) => sum + Number(inv.totalPrice), 0),
};
relevantData.push(
`Invoice Summary: Total invoices: ${summary.total}, Paid: ${summary.paid}, Pending: ${summary.pending}, Total amount: $${summary.totalAmount}`,
);
sources.push("Invoices Database");
}
} catch (error) {
this.logger.warn("Failed to fetch invoice data", error);
}
}
private hasOrderKeywords(keywords: string[]): boolean {
const orderTerms = ["order", "purchase", "buy", "cart", "سفارش", "خرید"];
return keywords.some((keyword) => orderTerms.includes(keyword));
}
private async getPaymentData(keywords: string[], userId: string, relevantData: string[], sources: string[]): Promise<void> {
if (this.hasPaymentKeywords(keywords)) {
try {
// Find user's businesses first
const userBusinesses = await this.em.find(Business, { danakSubscriptionId: userId });
if (userBusinesses.length > 0) {
const businessIds = userBusinesses.map((b) => b.id);
const payments = await this.em.find(Payment, { business: { $in: businessIds } }, { limit: 10, orderBy: { createdAt: "DESC" } });
const summary = {
total: payments.length,
successful: payments.filter((pay) => pay.status === PaymentStatus.COMPLETED).length,
failed: payments.filter((pay) => pay.status === PaymentStatus.FAILED).length,
totalAmount: payments.reduce((sum, pay) => sum + Number(pay.amount), 0),
};
relevantData.push(
`Payment Summary: Total payments: ${summary.total}, Successful: ${summary.successful}, Failed: ${summary.failed}, Total amount: $${summary.totalAmount}`,
);
sources.push("Payments Database");
}
} catch (error) {
this.logger.warn("Failed to fetch payment data", error);
}
}
}
private async getCompanyData(keywords: string[], userId: string, relevantData: string[], sources: string[]): Promise<void> {
if (this.hasCompanyKeywords(keywords)) {
try {
// Find user's businesses first
const userBusinesses = await this.em.find(Business, { danakSubscriptionId: userId });
if (userBusinesses.length > 0) {
const businessIds = userBusinesses.map((b) => b.id);
const companies = await this.em.find(Company, { business: { $in: businessIds } }, { limit: 5, populate: ["industry"] });
companies.forEach((company) => {
relevantData.push(`Company: ${company.name} - Industry: ${company.industry?.title || "N/A"} - Status: ${company.status}`);
sources.push("Companies Database");
});
}
} catch (error) {
this.logger.warn("Failed to fetch company data", error);
}
}
}
private async getTicketData(keywords: string[], userId: string, relevantData: string[], sources: string[]): Promise<void> {
if (this.hasTicketKeywords(keywords)) {
try {
// Find user's businesses first
const userBusinesses = await this.em.find(Business, { danakSubscriptionId: userId });
if (userBusinesses.length > 0) {
const businessIds = userBusinesses.map((b) => b.id);
const tickets = await this.em.find(
Ticket,
{ business: { $in: businessIds } },
{ limit: 10, orderBy: { createdAt: "DESC" }, populate: ["category"] },
);
const summary = {
total: tickets.length,
pending: tickets.filter((t) => t.status === TicketStatus.PENDING).length,
answered: tickets.filter((t) => t.status === TicketStatus.ANSWERED).length,
closed: tickets.filter((t) => t.status === TicketStatus.CLOSED).length,
categories: [...new Set(tickets.map((t) => t.category?.title || "Uncategorized"))],
};
relevantData.push(
`Support Tickets: Total: ${summary.total}, Pending: ${summary.pending}, Answered: ${summary.answered}, Closed: ${summary.closed}, Categories: ${summary.categories.join(", ")}`,
);
sources.push("Support Tickets Database");
}
} catch (error) {
this.logger.warn("Failed to fetch ticket data", error);
}
}
}
private async getIndustryData(keywords: string[], relevantData: string[], sources: string[]): Promise<void> {
if (this.hasIndustryKeywords(keywords)) {
try {
const industries = await this.em.find(Industry, {}, { limit: 10 });
const industryInfo = industries.map((ind) => `${ind.title}: ${ind.isActive ? "Active" : "Inactive"}`);
relevantData.push(`Available Industries: ${industryInfo.join("; ")}`);
sources.push("Industries Database");
} catch (error) {
this.logger.warn("Failed to fetch industry data", error);
}
}
}
private hasBusinessKeywords(keywords: string[]): boolean {
const businessTerms = ["business", "company", "startup", "enterprise", "organization", "firm"];
return keywords.some((keyword) => businessTerms.includes(keyword));
}
private hasInvoiceKeywords(keywords: string[]): boolean {
const invoiceTerms = ["invoice", "bill", "billing", "payment", "charge", "fee", "cost", "price"];
return keywords.some((keyword) => invoiceTerms.includes(keyword));
}
private hasPaymentKeywords(keywords: string[]): boolean {
const paymentTerms = ["payment", "pay", "transaction", "money", "refund", "charge", "card", "bank"];
return keywords.some((keyword) => paymentTerms.includes(keyword));
}
private hasCompanyKeywords(keywords: string[]): boolean {
const companyTerms = ["company", "corporation", "business", "organization", "enterprise"];
return keywords.some((keyword) => companyTerms.includes(keyword));
}
private hasTicketKeywords(keywords: string[]): boolean {
const ticketTerms = ["ticket", "support", "help", "issue", "problem", "bug", "error"];
return keywords.some((keyword) => ticketTerms.includes(keyword));
}
private hasIndustryKeywords(keywords: string[]): boolean {
const industryTerms = ["industry", "sector", "field", "domain", "market"];
return keywords.some((keyword) => industryTerms.includes(keyword));
private hasCategoryKeywords(keywords: string[]): boolean {
const categoryTerms = ["category", "type", "kind", "دسته", "دسته‌بندی"];
return keywords.some((keyword) => categoryTerms.includes(keyword));
}
}
@@ -3,38 +3,38 @@ import { StringOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
import { RunnableSequence } from "@langchain/core/runnables";
import { ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
import { EntityManager } from "@mikro-orm/core";
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { injectable } from "inversify";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { Company } from "../../companies/entities/company.entity";
import { Industry } from "../../industries/entities/industry.entity";
import { CHATBOT_CONSTANTS } from "../constants/chatbot.constants";
import { IChatContext, IChatbotResponse } from "../interfaces/chatbot.interface";
import { Logger } from "../../../core/logging/logger";
@Injectable()
export class LangChainService implements OnModuleInit {
private readonly logger = new Logger(LangChainService.name);
@injectable()
export class LangChainService {
private readonly logger: Logger;
private embeddings: GoogleGenerativeAIEmbeddings;
private llm: ChatGoogleGenerativeAI;
private vectorStore: MemoryVectorStore;
private textSplitter: RecursiveCharacterTextSplitter;
private isInitialized = false;
constructor(
private em: EntityManager,
private configService: ConfigService,
) {
constructor() {
this.logger = new Logger("LangChainService");
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error("GEMINI_API_KEY is required");
}
// Initialize Google Gemini components
this.embeddings = new GoogleGenerativeAIEmbeddings({
apiKey: this.configService.getOrThrow("GEMINI_API_KEY"),
apiKey,
modelName: "embedding-001", // Google's embedding model
});
this.llm = new ChatGoogleGenerativeAI({
apiKey: this.configService.getOrThrow("GEMINI_API_KEY"),
apiKey,
model: CHATBOT_CONSTANTS.DEFAULT_MODEL,
temperature: CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE,
maxOutputTokens: CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS,
@@ -45,18 +45,20 @@ export class LangChainService implements OnModuleInit {
chunkOverlap: 200,
separators: ["\n\n", "\n", ".", "!", "?", "؟", "!", ".", " ", ""],
});
}
async onModuleInit() {
await this.initializeVectorStore();
// Initialize vector store asynchronously
this.initializeVectorStore().catch((error) => {
this.logger.error("Failed to initialize vector store", error);
});
}
private async initializeVectorStore() {
try {
this.logger.log(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.LOADING_DATA);
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.LOADING_DATA);
// Load company and industry data for training
const documents = await this.loadCompanyAndIndustryDocuments();
// Load documents for training
// TODO: Load actual data from your database models (Product, Category, etc.)
const documents = await this.loadDocuments();
if (documents.length === 0) {
this.logger.warn(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.NO_DATA_FOUND);
@@ -68,7 +70,7 @@ export class LangChainService implements OnModuleInit {
// Split documents into chunks
const splitDocs = await this.textSplitter.splitDocuments(documents);
this.logger.log(
this.logger.info(
CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.DOCUMENTS_SPLIT.replace("{totalDocs}", documents.length.toString()).replace(
"{chunks}",
splitDocs.length.toString(),
@@ -79,7 +81,7 @@ export class LangChainService implements OnModuleInit {
this.vectorStore = await MemoryVectorStore.fromDocuments(splitDocs, this.embeddings);
this.isInitialized = true;
this.logger.log(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.VECTOR_STORE_READY);
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.VECTOR_STORE_READY);
} catch (error) {
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.VECTOR_STORE_ERROR, error);
// Fallback to empty vector store
@@ -88,131 +90,21 @@ export class LangChainService implements OnModuleInit {
}
}
private async loadCompanyAndIndustryDocuments(): Promise<Document[]> {
private async loadDocuments(): Promise<Document[]> {
const documents: Document[] = [];
const em = this.em.fork();
try {
// Load companies data with their products and services
const companies = await em.find(Company, { isActive: true, deletedAt: null }, { populate: ["industry", "business", "products", "services"] });
companies.forEach((company) => {
// Company basic information using template
const companyContent = this.replaceTemplate(CHATBOT_CONSTANTS.COMPANY_DATA_TEMPLATES.COMPANY_INFO, {
name: company.name,
ceo: company.chiefExecutiveOfficer,
email: company.email,
phone: company.phone,
registrationNumber: company.identificationNumber,
establishmentDate: new Date(company.dateOfEstablishment).toLocaleDateString("fa-IR"),
address: company.address,
website: company.websiteUrl || CHATBOT_CONSTANTS.DEFAULT_VALUES.NO_WEBSITE,
description: company.description,
industry: company.industry?.title || CHATBOT_CONSTANTS.DEFAULT_VALUES.UNSPECIFIED,
status: company.status,
business: company.business?.name || CHATBOT_CONSTANTS.DEFAULT_VALUES.UNSPECIFIED,
});
documents.push(
new Document({
pageContent: companyContent,
metadata: {
type: "company",
id: company.id,
name: company.name,
industry: company.industry?.title,
businessId: company.business?.id,
status: company.status,
},
}),
);
// Company products using template
if (company.products && company.products.getItems().length > 0) {
company.products.getItems().forEach((product) => {
const productContent = this.replaceTemplate(CHATBOT_CONSTANTS.COMPANY_DATA_TEMPLATES.COMPANY_PRODUCT, {
companyName: company.name,
productTitle: product.title,
industry: company.industry?.title || CHATBOT_CONSTANTS.DEFAULT_VALUES.UNSPECIFIED,
companyDescription: company.description,
companyAddress: company.address,
companyPhone: company.phone,
companyEmail: company.email,
});
documents.push(
new Document({
pageContent: productContent,
metadata: {
type: "product",
id: product.id,
title: product.title,
companyId: company.id,
companyName: company.name,
industry: company.industry?.title,
},
}),
);
});
}
// Company services using template
if (company.services && company.services.getItems().length > 0) {
company.services.getItems().forEach((service) => {
const serviceContent = this.replaceTemplate(CHATBOT_CONSTANTS.COMPANY_DATA_TEMPLATES.COMPANY_SERVICE, {
companyName: company.name,
serviceTitle: service.title,
industry: company.industry?.title || CHATBOT_CONSTANTS.DEFAULT_VALUES.UNSPECIFIED,
companyDescription: company.description,
companyAddress: company.address,
companyPhone: company.phone,
companyEmail: company.email,
});
documents.push(
new Document({
pageContent: serviceContent,
metadata: {
type: "service",
id: service.id,
title: service.title,
companyId: company.id,
companyName: company.name,
industry: company.industry?.title,
},
}),
);
});
}
});
// Load industries data using template
const industries = await em.find(Industry, { isActive: true, deletedAt: null }, { populate: ["companies", "business"] });
industries.forEach((industry) => {
const companiesInIndustry = industry.companies?.getItems().filter((c) => c.isActive && !c.deletedAt) || [];
const industryContent = this.replaceTemplate(CHATBOT_CONSTANTS.COMPANY_DATA_TEMPLATES.INDUSTRY_INFO, {
title: industry.title,
status: industry.isActive ? CHATBOT_CONSTANTS.DEFAULT_VALUES.ACTIVE : CHATBOT_CONSTANTS.DEFAULT_VALUES.INACTIVE,
companiesCount: companiesInIndustry.length.toString(),
business: industry.business?.name || CHATBOT_CONSTANTS.DEFAULT_VALUES.UNSPECIFIED,
companiesList: companiesInIndustry.map((company) => `- ${company.name}`).join("\n"),
});
documents.push(
new Document({
pageContent: industryContent,
metadata: {
type: "industry",
id: industry.id,
title: industry.title,
isActive: industry.isActive,
companiesCount: companiesInIndustry.length,
businessId: industry.business?.id,
},
}),
);
});
// TODO: Load actual data from your database
// Example:
// const products = await ProductModel.find({ ... }).limit(100);
// products.forEach((product) => {
// documents.push(
// new Document({
// pageContent: `Product: ${product.title_fa} - Description: ${product.description}`,
// metadata: { type: "product", id: product._id.toString() },
// }),
// );
// });
// Add comprehensive Farsi guidance documents from constants
const companyGuidanceDocuments = [
@@ -252,7 +144,7 @@ export class LangChainService implements OnModuleInit {
documents.push(...companyGuidanceDocuments);
this.logger.log(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.DOCUMENTS_LOADED.replace("{count}", documents.length.toString()));
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.DOCUMENTS_LOADED.replace("{count}", documents.length.toString()));
return documents;
} catch (error) {
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.LOADING_ERROR, error);
@@ -363,7 +255,8 @@ export class LangChainService implements OnModuleInit {
}
async refreshVectorStore(): Promise<void> {
this.logger.log(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.REFRESHING_VECTOR_STORE);
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.REFRESHING_VECTOR_STORE);
this.isInitialized = false;
await this.initializeVectorStore();
}
@@ -372,17 +265,6 @@ export class LangChainService implements OnModuleInit {
return Math.ceil(text.length / 4);
}
/**
* Replace placeholders in template with actual values
*/
private replaceTemplate(template: string, values: Record<string, string>): string {
let result = template;
Object.entries(values).forEach(([key, value]) => {
result = result.replace(new RegExp(`{${key}}`, "g"), value);
});
return result;
}
get initialized(): boolean {
return this.isInitialized;
}
+19 -14
View File
@@ -1,29 +1,32 @@
import { GoogleGenerativeAI, HarmBlockThreshold, HarmCategory } from "@google/generative-ai";
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { inject, injectable } from "inversify";
import { DataContextService } from "./data-context.service";
import { Logger } from "../../../core/logging/logger";
import { IOCTYPES } from "../../../IOC/ioc.types";
import { CHATBOT_CONSTANTS } from "../constants/chatbot.constants";
import { IChatContext, IChatbotResponse, ILLMConfig } from "../interfaces/chatbot.interface";
@Injectable()
@injectable()
export class LLMService {
private readonly logger = new Logger(LLMService.name);
private readonly logger: Logger;
private genAI: GoogleGenerativeAI;
private config: ILLMConfig;
constructor(
private configService: ConfigService,
private dataContextService: DataContextService,
) {
constructor(@inject(IOCTYPES.ChatbotDataContextService) private dataContextService: DataContextService) {
this.logger = new Logger("LLMService");
this.config = {
model: this.configService.get("GEMINI_MODEL", CHATBOT_CONSTANTS.DEFAULT_MODEL),
temperature: Number(this.configService.get("GEMINI_TEMPERATURE", CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE)),
maxTokens: Number(this.configService.get("GEMINI_MAX_TOKENS", CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS)),
topP: Number(this.configService.get("GEMINI_TOP_P", 0.95)),
apiKey: this.configService.getOrThrow("GEMINI_API_KEY"),
model: process.env.GEMINI_MODEL || CHATBOT_CONSTANTS.DEFAULT_MODEL,
temperature: Number(process.env.GEMINI_TEMPERATURE || CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE),
maxTokens: Number(process.env.GEMINI_MAX_TOKENS || CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS),
topP: Number(process.env.GEMINI_TOP_P || "0.95"),
apiKey: process.env.GEMINI_API_KEY || "",
};
if (!this.config.apiKey) {
throw new Error("GEMINI_API_KEY is required");
}
this.genAI = new GoogleGenerativeAI(this.config.apiKey);
}
@@ -162,7 +165,9 @@ export class LLMService {
}
const history = [];
const recentHistory = context.conversationHistory.slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY).filter((msg) => msg.type !== "system");
const recentHistory = context.conversationHistory
.slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY)
.filter((msg) => msg.type !== "system");
for (const msg of recentHistory) {
history.push({
@@ -1,10 +1,12 @@
import { Injectable, Logger } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { inject, injectable } from "inversify";
import { Socket } from "socket.io";
import { WebSocketMessage } from "../../../common/enums/message.enum";
import { ITokenPayload } from "../../auth/interfaces/IToken-payload";
import { extractTokenFromClient } from "../../utils/providers/extract-token.utils";
import { AuthTokenPayload } from "../../../common/types/jwt.type";
import { jwtExpiredErr } from "../../../core/app/app.errors";
import { Logger } from "../../../core/logging/logger";
import { IOCTYPES } from "../../../IOC/ioc.types";
import { TokenService } from "../../token/token.service";
import { WEBSOCKET_EVENTS } from "../constants/chatbot.constants";
import { AuthenticationResult, WebSocketErrorContext } from "../interfaces/websocket.interface";
@@ -12,11 +14,35 @@ import { AuthenticationResult, WebSocketErrorContext } from "../interfaces/webso
* Service responsible for WebSocket authentication logic
* Follows single responsibility principle and provides reusable authentication methods
*/
@Injectable()
@injectable()
export class WebSocketAuthService {
private readonly logger = new Logger(WebSocketAuthService.name);
private readonly logger: Logger;
constructor(private readonly jwtService: JwtService) {}
constructor(@inject(IOCTYPES.TokenService) private readonly tokenService: TokenService) {
this.logger = new Logger("WebSocketAuthService");
}
/**
* Extracts token from WebSocket client
*/
private extractTokenFromClient(client: Socket): string | null {
// Try to get token from query parameters
const token = client.handshake.query.token as string;
if (token) {
return token;
}
// Try to get token from auth header
const authHeader = client.handshake.headers.authorization;
if (authHeader && typeof authHeader === "string") {
const parts = authHeader.split(" ");
if (parts.length === 2 && parts[0] === "Bearer") {
return parts[1];
}
}
return null;
}
/**
* Authenticates a WebSocket client and returns the result
@@ -26,7 +52,7 @@ export class WebSocketAuthService {
*/
async authenticateClient(client: Socket): Promise<AuthenticationResult> {
try {
const token = extractTokenFromClient(client);
const token = this.extractTokenFromClient(client);
if (!token) {
this.logAuthenticationAttempt(client, false, "No token provided");
@@ -36,9 +62,9 @@ export class WebSocketAuthService {
};
}
const payload = await this.jwtService.verifyAsync<ITokenPayload>(token);
const payload = this.tokenService.verifyToken(token);
if (!payload?.id) {
if (!payload?.sub) {
this.logAuthenticationAttempt(client, false, "Invalid token payload");
return {
success: false,
@@ -46,14 +72,14 @@ export class WebSocketAuthService {
};
}
// Store user data in client
client.data.user = payload;
// Store user data in client (convert sub to id for compatibility)
client.data.user = { id: payload.sub, sub: payload.sub };
this.logAuthenticationAttempt(client, true, undefined, payload.id);
this.logAuthenticationAttempt(client, true, undefined, payload.sub);
return {
success: true,
user: payload,
user: { id: payload.sub, sub: payload.sub },
};
} catch (error) {
const errorMessage = this.getAuthErrorMessage(error);
@@ -90,7 +116,7 @@ export class WebSocketAuthService {
* @param client - Socket.IO client
* @param user - Authenticated user payload
*/
emitAuthenticationSuccess(client: Socket, user: ITokenPayload): void {
emitAuthenticationSuccess(client: Socket, user: { id: string; sub: string }): void {
client.emit(WEBSOCKET_EVENTS.AUTHENTICATED, {
message: WebSocketMessage.AUTHENTICATED,
userId: user.id,
@@ -151,7 +177,7 @@ export class WebSocketAuthService {
};
if (success) {
this.logger.log(`WebSocket authentication successful`, logData);
this.logger.info(`WebSocket authentication successful`, logData);
} else {
this.logger.warn(`WebSocket authentication failed`, logData);
}
@@ -161,7 +187,7 @@ export class WebSocketAuthService {
* Maps JWT errors to user-friendly messages
*/
private getAuthErrorMessage(error: any): string {
if (error?.name === "TokenExpiredError") {
if (error instanceof jwtExpiredErr || error?.name === "TokenExpiredError") {
return WebSocketMessage.TOKEN_EXPIRED;
}