chat bot 3

This commit is contained in:
morteza-mortezai
2025-11-29 12:48:04 +03:30
parent db266a1cd6
commit 61d1579446
19 changed files with 405 additions and 1237 deletions
+2 -4
View File
@@ -56,7 +56,6 @@ import { ChatMessageRepo, createChatMessageRepo } from "../modules/chat/reposito
import { WsAuthService } from "../modules/chat/wsAuth.service";
import { ChatbotService } from "../modules/chatbot/providers/chatbot.service";
import { DataContextService } from "../modules/chatbot/providers/data-context.service";
import { LangChainService } from "../modules/chatbot/providers/langchain.service";
import { LLMService } from "../modules/chatbot/providers/llm.service";
import { WebSocketAuthService } from "../modules/chatbot/providers/websocket-auth.service";
import { ChatSessionRepository, createChatSessionRepository } from "../modules/chatbot/repositories/chat-session.repository";
@@ -206,7 +205,6 @@ const containerModules = new AsyncContainerModule(async (bind) => {
bind<CacheService>(IOCTYPES.CacheService).to(CacheService).inSingletonScope();
bind<ChatbotService>(IOCTYPES.ChatbotService).to(ChatbotService).inSingletonScope();
bind<LLMService>(IOCTYPES.ChatbotLLMService).to(LLMService).inSingletonScope();
bind<LangChainService>(IOCTYPES.ChatbotLangChainService).to(LangChainService).inSingletonScope();
bind<DataContextService>(IOCTYPES.ChatbotDataContextService).to(DataContextService).inSingletonScope();
bind<WebSocketAuthService>(IOCTYPES.ChatbotWebSocketAuthService).to(WebSocketAuthService).inSingletonScope();
// #endregion
@@ -291,8 +289,8 @@ const containerModules = new AsyncContainerModule(async (bind) => {
bind<AboutUsRepo>(IOCTYPES.AboutUsRepo).toDynamicValue(CreateAboutUsRepo).inSingletonScope();
bind<SiteSettingRepo>(IOCTYPES.SiteSettingRepo).toDynamicValue(CreateSiteSettingRepo).inSingletonScope();
bind<NewsletterRepo>(IOCTYPES.NewsletterRepo).toDynamicValue(CreateNewsletterRepo).inSingletonScope();
bind<ChatSessionRepository>(IOCTYPES.ChatSessionRepository).toDynamicValue(createChatSessionRepository).inSingletonScope();
bind<ChatMessageRepository>(IOCTYPES.ChatMessageRepository).toDynamicValue(createChatMessageRepository).inSingletonScope();
bind<ChatSessionRepository>(IOCTYPES.ChatbotChatSessionRepository).toDynamicValue(createChatSessionRepository).inSingletonScope();
bind<ChatMessageRepository>(IOCTYPES.ChatbotChatMessageRepository).toDynamicValue(createChatMessageRepository).inSingletonScope();
// #endregion
});
+2 -3
View File
@@ -48,7 +48,6 @@ export const IOCTYPES = {
OrderQueue: Symbol.for("OrderQueue"),
ChatbotService: Symbol.for("ChatbotService"),
ChatbotLLMService: Symbol.for("ChatbotLLMService"),
ChatbotLangChainService: Symbol.for("ChatbotLangChainService"),
ChatbotDataContextService: Symbol.for("ChatbotDataContextService"),
ChatbotGateway: Symbol.for("ChatbotGateway"),
ChatbotWebSocketAuthService: Symbol.for("ChatbotWebSocketAuthService"),
@@ -137,8 +136,8 @@ export const IOCTYPES = {
ContactUsRepo: Symbol.for("ContactUsRepo"),
AboutUsRepo: Symbol.for("AboutUsRepo"),
NewsletterRepo: Symbol.for("NewsletterRepo"),
ChatSessionRepository: Symbol.for("ChatSessionRepository"),
ChatMessageRepository: Symbol.for("ChatMessageRepository"),
ChatbotChatSessionRepository: Symbol.for("ChatbotChatSessionRepository"),
ChatbotChatMessageRepository: Symbol.for("ChatbotChatMessageRepository"),
// #endregion
Logger: Symbol.for("Logger"),
ZarinPalGateway: Symbol.for("ZarinPalGateway"),
+14
View File
@@ -329,3 +329,17 @@ export const enum RoleMessage {
PermissionsNotEmpty = "دسترسی‌ها نباید خالی باشد",
RoleExist = "این نقش قبلا ثبت شده است",
}
export const enum WebSocketMessage {
AUTHENTICATED = "احراز هویت موفقیت‌آمیز",
AUTHENTICATION_REQUIRED = "احراز هویت ضروری است",
INVALID_TOKEN = "توکن احراز هویت نامعتبر است",
TOKEN_EXPIRED = "توکن منقضی شده است",
USER_NOT_AUTHENTICATED = "کاربر احراز هویت نشده است",
CONNECTION_FAILED = "اتصال برقرار نشد",
SESSION_CREATED = "جلسه چت با موفقیت ایجاد شد",
SESSION_CREATION_FAILED = "ایجاد جلسه چت با شکست مواجه شد",
SESSION_NOT_FOUND = "جلسه چت یافت نشد",
CHAT_JOINED = "با موفقیت به چت متصل شدید",
LLM_SERVICE_ERROR = "سرویس هوش مصنوعی موقتاً در دسترس نیست",
}
@@ -1,6 +1,5 @@
import { Expose } from "class-transformer";
import { IsOptional, IsString, MaxLength } from "class-validator";
import { isValidObjectId } from "mongoose";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { IsValidId } from "../../../common/decorator/validation.decorator";
@@ -1,15 +1,13 @@
import { Expose } from "class-transformer";
import { IsNotEmpty } from "class-validator";
import { isValidObjectId } from "mongoose";
import { ApiProperty } from "../../../common/decorator/swggerDocs";
import { CommonMessage } from "../../../common/enums/message.enum";
import { IsValidId } from "../../../common/decorator/validation.decorator";
import { ChatSessionModel } from "../models/chat-session.model";
export class SessionIdParamDto {
@Expose()
@IsNotEmpty({ message: CommonMessage.SESSION_ID_REQUIRED || "Session ID is required" })
@IsNotEmpty({ message: "Session ID is required" })
@IsValidId([ChatSessionModel])
@ApiProperty({ type: "string", description: "Session id of the entity", example: "66eff8c0c6ad5530b996c432" })
sessionId!: string;
+7 -16
View File
@@ -53,8 +53,8 @@ class ChatbotController extends BaseController {
return this.response({ data });
}
@ApiOperation("Send a message in a chat session (Enhanced with LangChain + Fallback)")
@ApiResponse("Message sent successfully with enhanced AI", HttpStatus.Created)
@ApiOperation("Send a message in a chat session")
@ApiResponse("Message sent successfully", HttpStatus.Created)
@ApiModel(SendMessageDto)
@ApiAuth()
@httpPost("/messages", Guard.authUser(), ValidationMiddleware.validateInput(SendMessageDto))
@@ -64,8 +64,8 @@ class ChatbotController extends BaseController {
return this.response({ data }, HttpStatus.Created);
}
@ApiOperation("Send a message and get streaming response (Enhanced with LangChain + Fallback)")
@ApiResponse("Enhanced streaming response initiated")
@ApiOperation("Send a message and get streaming response")
@ApiResponse("Streaming response initiated")
@ApiModel(SendMessageDto)
@ApiAuth()
@httpPost("/messages/stream", Guard.authUser(), ValidationMiddleware.validateInput(SendMessageDto))
@@ -76,7 +76,6 @@ class ChatbotController extends BaseController {
): Promise<void> {
const user = req.user as IUser;
try {
// Now uses LangChain by default with automatic Gemini fallback
const { userMessage, streamGenerator } = await this.chatbotService.sendMessageStream(user._id.toString(), sendDto);
// Set headers for Server-Sent Events
@@ -86,10 +85,10 @@ class ChatbotController extends BaseController {
res.setHeader("Access-Control-Allow-Origin", "*");
// Send initial user message
res.write(`data: ${JSON.stringify({ type: "user_message", data: userMessage, enhanced: true })}\n\n`);
res.write(`data: ${JSON.stringify({ type: "user_message", data: userMessage })}\n\n`);
// Start streaming bot response
res.write(`data: ${JSON.stringify({ type: "bot_response_start", provider: "enhanced" })}\n\n`);
res.write(`data: ${JSON.stringify({ type: "bot_response_start" })}\n\n`);
const stream = await streamGenerator();
for await (const chunk of stream) {
@@ -104,20 +103,12 @@ class ChatbotController extends BaseController {
} catch (error) {
console.error(error);
res.write(
`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ هوشمند. LangChain با خطا مواجه شد و به Gemini بازگشت ⚠️" })}\n\n`,
`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ هوشمند. لطفاً دوباره تلاش کنید ⚠️" })}\n\n`,
);
res.end();
}
}
@ApiOperation("Refresh LangChain training data from database")
@ApiResponse("Training data refreshed successfully")
@ApiAuth()
@httpPost("/training/refresh", Guard.authUser())
public async refreshTrainingData(@request() _req: Request) {
const data = await this.chatbotService.refreshLangChainData();
return this.response({ data });
}
@ApiOperation("Close a chat session")
@ApiResponse("Chat session closed successfully")
-539
View File
@@ -1,539 +0,0 @@
import { Logger, UseFilters, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
import {
ConnectedSocket,
MessageBody,
OnGatewayConnection,
OnGatewayDisconnect,
OnGatewayInit,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from "@nestjs/websockets";
import { Server, Socket } from "socket.io";
import { WEBSOCKET_EVENTS } from "./constants/chatbot.constants";
import { AuthenticateDto, CreateSessionDto, JoinChatDto, LeaveChatDto, SendMessageWebSocketDto, TypingDto } from "./DTO/websocket-events.dto";
import { WebSocketAuthGuard } from "./guards/websocket-auth.guard";
import { AuthenticatedSocket, ConnectedUserInfo, WebSocketResponse } from "./interfaces/websocket.interface";
import { ChatbotService } from "./providers/chatbot.service";
import { WebSocketAuthService } from "./providers/websocket-auth.service";
import { WebSocketMessage } from "../../common/enums/message.enum";
import { WsExceptionFilter } from "../../core/filters/ws-exception.filter";
@WebSocketGateway({
cors: { origin: "*", credentials: true },
namespace: "/chat",
})
@UsePipes(new ValidationPipe({ transform: true }))
@UseFilters(WsExceptionFilter)
export class ChatbotGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
private readonly logger = new Logger(ChatbotGateway.name);
// Connection state management
private readonly connectedUsers = new Map<string, ConnectedUserInfo>();
private readonly userSessions = new Map<string, Set<string>>();
constructor(
private readonly chatbotService: ChatbotService,
private readonly authService: WebSocketAuthService,
) {}
/**
* Gateway initialization lifecycle hook
*/
afterInit(server: Server): void {
this.logger.log("WebSocket Gateway initialized", {
namespace: "/chat",
cors: true,
serverInstance: !!server,
});
}
/**
* Handles new WebSocket connections with authentication
* Implements fail-fast principle for security
*/
async handleConnection(client: Socket): Promise<void> {
const connectionContext = {
clientId: client.id,
clientIP: client.handshake.address,
userAgent: client.handshake.headers["user-agent"],
timestamp: new Date().toISOString(),
};
this.logger.log("New WebSocket connection attempt", connectionContext);
try {
const authResult = await this.authService.authenticateClient(client);
if (!authResult.success) {
this.authService.handleAuthenticationFailure(client, authResult.error!);
return;
}
const user = authResult.user!;
this.registerUserConnection(client.id, user.id);
this.authService.emitAuthenticationSuccess(client, user);
this.logger.log("WebSocket connection established", {
...connectionContext,
userId: user.id,
authenticated: true,
});
} catch (error) {
this.logger.error("WebSocket connection error", {
...connectionContext,
error: error instanceof Error ? error.message : String(error),
});
this.authService.handleAuthenticationFailure(client, WebSocketMessage.CONNECTION_FAILED);
}
}
/**
* Handles client disconnections with cleanup
*/
handleDisconnect(client: Socket): void {
const userInfo = this.connectedUsers.get(client.id);
this.logger.log("WebSocket client disconnected", {
clientId: client.id,
userId: userInfo?.userId,
sessionId: userInfo?.sessionId,
});
if (userInfo) {
this.cleanupUserConnection(client, userInfo);
}
this.connectedUsers.delete(client.id);
}
// ============================================================================
// MESSAGE HANDLERS
// ============================================================================
/**
* Handles explicit authentication requests (for re-authentication)
*/
@SubscribeMessage(WEBSOCKET_EVENTS.AUTHENTICATE)
async handleAuthenticate(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() _data: AuthenticateDto): Promise<void> {
try {
if (!this.authService.isClientAuthenticated(client)) {
throw new Error(WebSocketMessage.USER_NOT_AUTHENTICATED);
}
this.emitSuccessResponse(client, WEBSOCKET_EVENTS.AUTHENTICATED, {
message: WebSocketMessage.AUTHENTICATED,
userId: client.data.user.id,
});
} catch (error) {
this.handleMessageError(client, error, WEBSOCKET_EVENTS.AUTHENTICATE);
}
}
/**
* Handles chat session creation
*/
@SubscribeMessage(WEBSOCKET_EVENTS.CREATE_SESSION)
@UseGuards(WebSocketAuthGuard)
async handleCreateSession(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: CreateSessionDto): Promise<void> {
try {
const userId = this.authService.getUserId(client);
if (!userId) {
throw new Error(WebSocketMessage.USER_NOT_AUTHENTICATED);
}
const session = await this.chatbotService.createChatSession(userId, { title: data.title });
this.emitSuccessResponse(client, WEBSOCKET_EVENTS.SESSION_CREATED, {
message: WebSocketMessage.SESSION_CREATED,
session,
});
this.logger.log("Chat session created", {
sessionId: session.id,
userId,
title: data.title,
});
} catch (error) {
this.handleMessageError(client, error, WEBSOCKET_EVENTS.SESSION_ERROR);
this.emitErrorResponse(client, WEBSOCKET_EVENTS.SESSION_ERROR, {
message: WebSocketMessage.SESSION_CREATION_FAILED,
});
}
}
/**
* Handles joining a chat session
*/
@SubscribeMessage(WEBSOCKET_EVENTS.JOIN_CHAT)
@UseGuards(WebSocketAuthGuard)
async handleJoinChat(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: JoinChatDto): Promise<void> {
try {
const userId = this.authService.getUserId(client);
if (!userId) {
throw new Error(WebSocketMessage.USER_NOT_AUTHENTICATED);
}
// Verify session ownership
await this.chatbotService.getChatSession(data.sessionId, userId);
// Leave previous session if any
await this.leavePreviousSession(client);
// Join new session
await client.join(`session_${data.sessionId}`);
this.updateUserSession(client.id, userId, data.sessionId);
// Emit success events
this.emitSuccessResponse(client, WEBSOCKET_EVENTS.CHAT_JOINED, {
message: WebSocketMessage.CHAT_JOINED,
sessionId: data.sessionId,
});
// Notify others in session
client.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.USER_JOINED, {
userId,
sessionId: data.sessionId,
});
this.logger.log("User joined chat session", {
userId,
sessionId: data.sessionId,
clientId: client.id,
});
} catch (error) {
this.handleMessageError(client, error, WEBSOCKET_EVENTS.JOIN_CHAT);
this.emitErrorResponse(client, WEBSOCKET_EVENTS.ERROR, {
message: WebSocketMessage.SESSION_NOT_FOUND,
});
}
}
/**
* Handles leaving a chat session
*/
@SubscribeMessage(WEBSOCKET_EVENTS.LEAVE_CHAT)
@UseGuards(WebSocketAuthGuard)
async handleLeaveChat(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: LeaveChatDto): Promise<void> {
try {
const userId = this.authService.getUserId(client);
if (!userId) {
throw new Error(WebSocketMessage.USER_NOT_AUTHENTICATED);
}
await client.leave(`session_${data.sessionId}`);
this.clearUserSession(client.id);
// Emit success events
this.emitSuccessResponse(client, WEBSOCKET_EVENTS.CHAT_LEFT, {
sessionId: data.sessionId,
});
// Notify others in session
client.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.USER_LEFT, {
userId,
sessionId: data.sessionId,
});
this.logger.log("User left chat session", {
userId,
sessionId: data.sessionId,
clientId: client.id,
});
} catch (error) {
this.handleMessageError(client, error, WEBSOCKET_EVENTS.LEAVE_CHAT);
}
}
/**
* Handles sending messages in chat
*/
@SubscribeMessage(WEBSOCKET_EVENTS.SEND_MESSAGE)
@UseGuards(WebSocketAuthGuard)
async handleSendMessage(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: SendMessageWebSocketDto): Promise<void> {
try {
const userId = this.authService.getUserId(client);
if (!userId) {
throw new Error(WebSocketMessage.USER_NOT_AUTHENTICATED);
}
// Start typing indicator
this.emitTypingIndicator(data.sessionId, "bot", true);
// Send message through service
const userMessage = await this.chatbotService.sendMessage(userId, {
sessionId: data.sessionId,
content: data.content,
responseToId: data.responseToId,
metadata: data.metadata,
});
// Emit user message confirmation
this.server.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.MESSAGE_RECEIVED, {
message: userMessage,
sessionId: data.sessionId,
enhanced: true,
});
this.logger.log("Message sent", {
userId,
sessionId: data.sessionId,
messageId: userMessage.id,
});
// Generate bot response asynchronously
this.generateBotResponseAsync(data.sessionId, userId, data.content);
} catch (error) {
this.handleMessageError(client, error, WEBSOCKET_EVENTS.MESSAGE_ERROR);
this.emitErrorResponse(client, WEBSOCKET_EVENTS.MESSAGE_ERROR, {
message: WebSocketMessage.LLM_SERVICE_ERROR,
});
this.emitTypingIndicator(data.sessionId, "bot", false);
}
}
/**
* Handles typing start events
*/
@SubscribeMessage(WEBSOCKET_EVENTS.TYPING_START)
@UseGuards(WebSocketAuthGuard)
async handleTypingStart(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: TypingDto): Promise<void> {
const userId = this.authService.getUserId(client);
if (!userId) return;
client.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.TYPING_START, {
userId,
type: "user",
sessionId: data.sessionId,
});
}
/**
* Handles typing stop events
*/
@SubscribeMessage(WEBSOCKET_EVENTS.TYPING_STOP)
@UseGuards(WebSocketAuthGuard)
async handleTypingStop(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() data: TypingDto): Promise<void> {
const userId = this.authService.getUserId(client);
if (!userId) return;
client.to(`session_${data.sessionId}`).emit(WEBSOCKET_EVENTS.TYPING_STOP, {
userId,
type: "user",
sessionId: data.sessionId,
});
}
// ============================================================================
// PRIVATE HELPER METHODS
// ============================================================================
/**
* Registers a new user connection
*/
private registerUserConnection(clientId: string, userId: string): void {
this.connectedUsers.set(clientId, { userId });
if (!this.userSessions.has(userId)) {
this.userSessions.set(userId, new Set());
}
this.userSessions.get(userId)!.add(clientId);
}
/**
* Updates user session information
*/
private updateUserSession(clientId: string, _userId: string, sessionId: string): void {
const userInfo = this.connectedUsers.get(clientId);
if (userInfo) {
userInfo.sessionId = sessionId;
}
}
/**
* Clears user session information
*/
private clearUserSession(clientId: string): void {
const userInfo = this.connectedUsers.get(clientId);
if (userInfo) {
userInfo.sessionId = undefined;
}
}
/**
* Cleans up user connection on disconnect
*/
private cleanupUserConnection(client: Socket, userInfo: ConnectedUserInfo): void {
// Remove from user sessions
const userSockets = this.userSessions.get(userInfo.userId);
if (userSockets) {
userSockets.delete(client.id);
if (userSockets.size === 0) {
this.userSessions.delete(userInfo.userId);
}
}
// Leave session room if joined
if (userInfo.sessionId) {
client.leave(`session_${userInfo.sessionId}`);
// Notify others in session
this.server.to(`session_${userInfo.sessionId}`).emit(WEBSOCKET_EVENTS.USER_LEFT, {
userId: userInfo.userId,
sessionId: userInfo.sessionId,
});
}
}
/**
* Handles leaving previous session when joining a new one
*/
private async leavePreviousSession(client: Socket): Promise<void> {
const userInfo = this.connectedUsers.get(client.id);
if (userInfo?.sessionId) {
await client.leave(`session_${userInfo.sessionId}`);
}
}
/**
* Emits typing indicators
*/
private emitTypingIndicator(sessionId: string, type: "user" | "bot", isTyping: boolean): void {
const event = isTyping ? WEBSOCKET_EVENTS.TYPING_START : WEBSOCKET_EVENTS.TYPING_STOP;
this.server.to(`session_${sessionId}`).emit(event, {
type,
sessionId,
});
}
/**
* Generates bot response asynchronously
*/
private async generateBotResponseAsync(sessionId: string, userId: string, userMessage: string): Promise<void> {
try {
// Start bot response
this.server.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.BOT_RESPONSE_START, {
sessionId,
provider: "enhanced",
});
// Get streaming response
const { streamGenerator } = await this.chatbotService.sendMessageStream(userId, {
sessionId,
content: userMessage,
});
const stream = await streamGenerator();
// Stream chunks to client
for await (const chunk of stream) {
if (chunk) {
this.server.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.BOT_RESPONSE_CHUNK, {
data: chunk,
sessionId,
});
}
}
// End bot response
this.server.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.BOT_RESPONSE_END, { sessionId });
this.emitTypingIndicator(sessionId, "bot", false);
this.logger.log("Bot response completed", { sessionId, userId });
} catch (error) {
this.logger.error("Bot response generation failed", {
sessionId,
userId,
error: error instanceof Error ? error.message : String(error),
});
this.server.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.ERROR, {
message: WebSocketMessage.LLM_SERVICE_ERROR,
sessionId,
});
this.emitTypingIndicator(sessionId, "bot", false);
}
}
/**
* Handles message-level errors with proper logging
*/
private handleMessageError(client: Socket, error: unknown, event: WEBSOCKET_EVENTS): void {
const errorContext = this.authService.createErrorContext(client, event);
this.logger.error(`WebSocket message error: ${event}`, {
...errorContext,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
}
/**
* Emits success response with consistent structure
*/
private emitSuccessResponse<T>(client: Socket, event: WEBSOCKET_EVENTS, data: T): void {
const response: WebSocketResponse<T> = {
...data,
status: "success",
timestamp: new Date().toISOString(),
};
console.log(data);
client.emit(event, response);
}
/**
* Emits error response with consistent structure
*/
private emitErrorResponse(client: Socket, event: WEBSOCKET_EVENTS, error: { message: string }): void {
const response: WebSocketResponse = {
status: "error",
message: error.message,
timestamp: new Date().toISOString(),
};
client.emit(event, response);
}
// ============================================================================
// PUBLIC UTILITY METHODS
// ============================================================================
/**
* Public method to emit bot responses (called from service)
*/
async emitBotResponse(sessionId: string, message: unknown): Promise<void> {
this.server.to(`session_${sessionId}`).emit(WEBSOCKET_EVENTS.BOT_RESPONSE, {
message,
sessionId,
});
}
/**
* Gets connected users in a session
*/
getSessionUsers(sessionId: string): string[] {
const users: string[] = [];
this.connectedUsers.forEach((userInfo) => {
if (userInfo.sessionId === sessionId) {
users.push(userInfo.userId);
}
});
return [...new Set(users)];
}
/**
* Checks if user is online
*/
isUserOnline(userId: string): boolean {
return this.userSessions.has(userId) && this.userSessions.get(userId)!.size > 0;
}
}
-23
View File
@@ -1,23 +0,0 @@
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { Module } from "@nestjs/common";
import { ChatbotController } from "./chatbot.controller";
import { ChatbotGateway } from "./chatbot.gateway";
import { ChatMessage } from "./entities/chat-message.entity";
import { ChatSession } from "./entities/chat-session.entity";
import { WebSocketAuthGuard } from "./guards/websocket-auth.guard";
import { ChatbotService } from "./providers/chatbot.service";
import { DataContextService } from "./providers/data-context.service";
import { LangChainService } from "./providers/langchain.service";
import { LLMService } from "./providers/llm.service";
import { WebSocketAuthService } from "./providers/websocket-auth.service";
import { AuthModule } from "../auth/auth.module";
import { UsersModule } from "../users/users.module";
@Module({
imports: [MikroOrmModule.forFeature([ChatSession, ChatMessage]), AuthModule, UsersModule],
controllers: [ChatbotController],
providers: [ChatbotService, ChatbotGateway, LLMService, LangChainService, DataContextService, WebSocketAuthGuard, WebSocketAuthService],
exports: [ChatbotService, ChatbotGateway],
})
export class ChatbotModule {}
@@ -1,48 +0,0 @@
import { Entity, EntityRepositoryType, Enum, ManyToOne, Opt, Property } from "@mikro-orm/core";
import { ChatSession } from "./chat-session.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
import { ChatMessageRepository } from "../repositories/chat-message.repository";
export enum MessageType {
USER = "user",
BOT = "bot",
SYSTEM = "system",
}
export enum MessageStatus {
SENT = "sent",
DELIVERED = "delivered",
READ = "read",
FAILED = "failed",
}
@Entity({ repository: () => ChatMessageRepository })
export class ChatMessage extends BaseEntity {
@Property({ type: "text" })
content!: string;
@Enum({ items: () => MessageType, nativeEnumName: "message_type" })
type!: MessageType;
@Enum({ items: () => MessageStatus, nativeEnumName: "message_status", default: MessageStatus.SENT })
status: MessageStatus & Opt;
@ManyToOne(() => ChatSession)
session!: ChatSession;
@ManyToOne(() => User, { nullable: true })
sender?: User;
@Property({ type: "json", nullable: true })
metadata?: Record<string, any>;
@Property({ nullable: true })
responseToId?: string;
@Property({ default: 0 })
tokensUsed: number = 0;
[EntityRepositoryType]?: ChatMessageRepository;
}
@@ -1,35 +0,0 @@
import { Collection, Entity, EntityRepositoryType, Enum, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core";
import { ChatMessage } from "./chat-message.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
import { ChatSessionRepository } from "../repositories/chat-session.repository";
export enum ChatSessionStatus {
ACTIVE = "active",
CLOSED = "closed",
ARCHIVED = "archived",
}
@Entity({ repository: () => ChatSessionRepository })
export class ChatSession extends BaseEntity {
@Property({ type: "varchar" })
title!: string;
@ManyToOne(() => User)
user!: User;
@Enum({ items: () => ChatSessionStatus, nativeEnumName: "chat_session_status", default: ChatSessionStatus.ACTIVE })
status: ChatSessionStatus & Opt;
@OneToMany(() => ChatMessage, (chatMessage) => chatMessage.session)
messages = new Collection<ChatMessage>(this);
@Property({ type: "json", nullable: true })
context?: Record<string, any>;
@Property({ nullable: true })
lastMessageAt?: Date;
[EntityRepositoryType]?: ChatSessionRepository;
}
@@ -1,55 +0,0 @@
import { CanActivate, ExecutionContext, Injectable, Logger } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { WsException } from "@nestjs/websockets";
import { Socket } from "socket.io";
import { WebSocketMessage } from "../../../common/enums/message.enum";
import { WEBSOCKET_EVENTS } from "../constants/chatbot.constants";
@Injectable()
export class WebSocketAuthGuard implements CanActivate {
private readonly logger = new Logger(WebSocketAuthGuard.name);
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const client: Socket = context.switchToWs().getClient<Socket>();
try {
// Get token from auth object or handshake query
const token = this.extractTokenFromHeader(client);
if (!token) {
this.logger.warn("No token provided in WebSocket connection");
throw new WsException({
event: WEBSOCKET_EVENTS.UNAUTHORIZED,
message: WebSocketMessage.AUTHENTICATION_REQUIRED,
});
}
// Verify JWT token
const payload = await this.jwtService.verifyAsync(token);
client.data.user = payload;
this.logger.log(`WebSocket authentication successful for user: ${payload.id}`);
return true;
} catch (error) {
this.logger.error("WebSocket authentication failed:", error);
client.emit(WEBSOCKET_EVENTS.UNAUTHORIZED, {
message: WebSocketMessage.INVALID_TOKEN,
});
throw new WsException({
event: WEBSOCKET_EVENTS.UNAUTHORIZED,
message: WebSocketMessage.INVALID_TOKEN,
});
}
}
private extractTokenFromHeader(client: Socket): string | undefined {
const authToken = client.handshake?.auth?.token || client.handshake?.query?.token;
return authToken;
}
}
@@ -1,13 +1,11 @@
import { Socket } from "socket.io";
import { ITokenPayload } from "../../auth/interfaces/IToken-payload";
/**
* Extended Socket interface with authenticated user data
*/
export interface AuthenticatedSocket extends Socket {
data: {
user: ITokenPayload;
user: { id: string; sub: string };
sessionId?: string;
};
}
@@ -47,6 +45,6 @@ export interface WebSocketResponse<T = any> {
*/
export interface AuthenticationResult {
success: boolean;
user?: ITokenPayload;
user?: { id: string; sub: string };
error?: string;
}
@@ -23,7 +23,7 @@ const chatMessageSchema = new Schema<IChatMessage>(
chatMessageSchema.index({ session: 1, createdAt: -1 });
chatMessageSchema.index({ session: 1, createdAt: 1 });
const ChatMessageModel = model<IChatMessage>("ChatMessage", chatMessageSchema);
const ChatMessageModel = model<IChatMessage>("ChatbotMessage", chatMessageSchema);
export { ChatMessageModel, MessageStatus, MessageType };
@@ -1,7 +1,6 @@
import { inject, injectable } from "inversify";
import { isValidObjectId } from "mongoose";
import { LangChainService } from "./langchain.service";
import { LLMService } from "./llm.service";
import { UserModel } from "../../user/models/user.model";
import { CreateChatSessionDto } from "../DTO/create-chat-session.dto";
@@ -17,26 +16,17 @@ 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()
export class ChatbotService {
private readonly logger: Logger;
private defaultProvider: LLMProvider;
constructor(
@inject(IOCTYPES.ChatbotLLMService) private llmService: LLMService,
@inject(IOCTYPES.ChatbotLangChainService) private langChainService: LangChainService,
@inject(IOCTYPES.ChatSessionRepository) private chatSessionRepo: ChatSessionRepository,
@inject(IOCTYPES.ChatMessageRepository) private chatMessageRepo: ChatMessageRepository,
@inject(IOCTYPES.ChatbotChatSessionRepository) private chatSessionRepo: ChatSessionRepository,
@inject(IOCTYPES.ChatbotChatMessageRepository) private chatMessageRepo: ChatMessageRepository,
) {
this.logger = new Logger("ChatbotService");
// Configure default provider - LangChain for enhanced responses
this.defaultProvider = process.env.DEFAULT_LLM_PROVIDER === "gemini" ? LLMProvider.GEMINI : LLMProvider.LANGCHAIN;
this.logger.info(`Using ${this.defaultProvider} as default LLM provider`);
this.logger.info("Using Google Gemini as LLM provider");
}
async createChatSession(userId: string, createDto: CreateChatSessionDto) {
@@ -82,24 +72,7 @@ export class ChatbotService {
return this.mapSessionToDto(session);
}
async sendMessage(userId: string, sendDto: SendMessageDto, provider?: LLMProvider) {
const selectedProvider = provider || this.defaultProvider;
// Use LangChain by default for enhanced responses
if (selectedProvider === LLMProvider.LANGCHAIN) {
try {
return await this.sendMessageWithLangChain(userId, sendDto);
} catch (error) {
this.logger.error("LangChain failed, falling back to Gemini", error);
// Fallback to Gemini service
return await this.sendMessageWithGemini(userId, sendDto);
}
} else {
return await this.sendMessageWithGemini(userId, sendDto);
}
}
private async sendMessageWithGemini(userId: string, sendDto: SendMessageDto) {
async sendMessage(userId: string, sendDto: SendMessageDto) {
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
throw new BadRequestError("Invalid session or user ID");
}
@@ -137,24 +110,7 @@ export class ChatbotService {
return this.mapMessageToDto(userMessage);
}
async sendMessageStream(userId: string, sendDto: SendMessageDto, provider?: LLMProvider) {
const selectedProvider = provider || this.defaultProvider;
// Use LangChain by default for enhanced streaming responses
if (selectedProvider === LLMProvider.LANGCHAIN) {
try {
return await this.sendMessageStreamWithLangChain(userId, sendDto);
} catch (error) {
this.logger.error("LangChain streaming failed, falling back to Gemini", error);
// Fallback to Gemini service
return await this.sendMessageStreamWithGemini(userId, sendDto);
}
} else {
return await this.sendMessageStreamWithGemini(userId, sendDto);
}
}
private async sendMessageStreamWithGemini(userId: string, sendDto: SendMessageDto) {
async sendMessageStream(userId: string, sendDto: SendMessageDto) {
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
throw new BadRequestError("Invalid session or user ID");
}
@@ -202,7 +158,7 @@ export class ChatbotService {
userPreferences: session.context,
};
// Return a function that generates the stream using original LLM service
// Return a function that generates the stream using LLM service
const streamGenerator = () => this.llmService.generateStreamResponse(sendDto.content, context);
return {
@@ -243,7 +199,7 @@ export class ChatbotService {
const llmResponse = await this.llmService.generateResponse(userMessage.content, context);
// Create bot message
const botMessage = await ChatMessageModel.create({
await ChatMessageModel.create({
content: llmResponse.message,
type: MessageType.BOT,
session: sessionId,
@@ -334,169 +290,4 @@ export class ChatbotService {
};
}
async sendMessageWithLangChain(userId: string, sendDto: SendMessageDto) {
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 NotFoundError("Chat session not found");
}
if (session.status !== ChatSessionStatus.ACTIVE) {
throw new BadRequestError("Cannot send message to inactive session");
}
const user = await UserModel.findById(userId);
if (!user) {
throw new NotFoundError("User not found");
}
// Create user message
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(sendDto.sessionId);
// Generate bot response using LangChain
this.generateBotResponseWithLangChain(sendDto.sessionId, userMessage._id.toString(), userId);
return this.mapMessageToDto(userMessage);
}
async sendMessageStreamWithLangChain(userId: string, sendDto: SendMessageDto) {
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 NotFoundError("Chat session not found");
}
if (session.status !== ChatSessionStatus.ACTIVE) {
throw new BadRequestError("Cannot send message to inactive session");
}
const user = await UserModel.findById(userId);
if (!user) {
throw new NotFoundError("User not found");
}
// Create user message
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(sendDto.sessionId);
// Get conversation history for context
const history = await this.chatMessageRepo.getConversationHistory(sendDto.sessionId, 20);
// Build context for LangChain
const context: IChatContext = {
userId,
sessionId: sendDto.sessionId,
conversationHistory: history.reverse().map((msg) => ({
content: msg.content,
type: msg.type as "user" | "bot" | "system",
timestamp: msg.createdAt,
metadata: msg.metadata,
})),
userPreferences: session.context,
};
// Return a function that generates the stream using LangChain
const streamGenerator = () => this.langChainService.generateStreamResponse(sendDto.content, context);
return {
userMessage: this.mapMessageToDto(userMessage),
streamGenerator,
};
}
private async generateBotResponseWithLangChain(sessionId: string, userMessageId: string, userId: string): Promise<void> {
try {
// Get conversation history
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,
conversationHistory: history.reverse().map((msg) => ({
content: msg.content,
type: msg.type as "user" | "bot" | "system",
timestamp: msg.createdAt,
metadata: msg.metadata,
})),
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 = 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(sessionId);
this.logger.info(`Generated LangChain bot response for session ${sessionId}`);
} catch (error) {
this.logger.error(`Failed to generate LangChain bot response for session ${sessionId}`, error);
// Fallback to regular LLM service
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.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");
}
}
}
@@ -15,13 +15,13 @@ export class DataContextService {
this.logger = new Logger("DataContextService");
}
async getRelevantContext(message: string, context: IChatContext): Promise<{ data: string[]; sources: string[] }> {
async getRelevantContext(_message: string, _context: IChatContext): Promise<{ data: string[]; sources: string[] }> {
const relevantData: string[] = [];
const sources: string[] = [];
try {
// Analyze message to determine what data to fetch
const keywords = this.extractKeywords(message);
// const keywords = this._extractKeywords(_message); // TODO: Use when implementing keyword-based filtering
// TODO: Implement actual data fetching based on your project's models
// For now, this returns empty data - you can extend this to fetch:
@@ -47,29 +47,33 @@ export class DataContextService {
}
}
private extractKeywords(message: string): string[] {
// TODO: Use this method when implementing keyword-based filtering
// @ts-expect-error - Method reserved for future use
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private _extractKeywords(_message: string): string[] {
const commonWords = ["the", "is", "at", "which", "on", "a", "an", "and", "or", "but", "in", "with", "to", "for", "of", "as", "by"];
return message
return _message
.toLowerCase()
.replace(/[^\w\s]/g, "")
.split(/\s+/)
.filter((word) => word.length > 2 && !commonWords.includes(word));
.filter((word: string) => word.length > 2 && !commonWords.includes(word));
}
// 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));
}
// TODO: Implement when needed
// private hasProductKeywords(keywords: string[]): boolean {
// const productTerms = ["product", "item", "goods", "merchandise", "کالا", "محصول"];
// return keywords.some(keyword => productTerms.includes(keyword));
// }
private hasOrderKeywords(keywords: string[]): boolean {
const orderTerms = ["order", "purchase", "buy", "cart", "سفارش", "خرید"];
return keywords.some((keyword) => orderTerms.includes(keyword));
}
// private hasOrderKeywords(keywords: string[]): boolean {
// const orderTerms = ["order", "purchase", "buy", "cart", "سفارش", "خرید"];
// return keywords.some(keyword => orderTerms.includes(keyword));
// }
private hasCategoryKeywords(keywords: string[]): boolean {
const categoryTerms = ["category", "type", "kind", "دسته", "دسته‌بندی"];
return keywords.some((keyword) => categoryTerms.includes(keyword));
}
// private hasCategoryKeywords(keywords: string[]): boolean {
// const categoryTerms = ["category", "type", "kind", "دسته", "دسته‌بندی"];
// return keywords.some(keyword => categoryTerms.includes(keyword));
// }
}
@@ -1,271 +0,0 @@
import { Document } from "@langchain/core/documents";
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 { injectable } from "inversify";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { CHATBOT_CONSTANTS } from "../constants/chatbot.constants";
import { IChatContext, IChatbotResponse } from "../interfaces/chatbot.interface";
import { Logger } from "../../../core/logging/logger";
@injectable()
export class LangChainService {
private readonly logger: Logger;
private embeddings: GoogleGenerativeAIEmbeddings;
private llm: ChatGoogleGenerativeAI;
private vectorStore: MemoryVectorStore;
private textSplitter: RecursiveCharacterTextSplitter;
private isInitialized = false;
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,
modelName: "embedding-001", // Google's embedding model
});
this.llm = new ChatGoogleGenerativeAI({
apiKey,
model: CHATBOT_CONSTANTS.DEFAULT_MODEL,
temperature: CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE,
maxOutputTokens: CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS,
});
this.textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
separators: ["\n\n", "\n", ".", "!", "?", "؟", "!", ".", " ", ""],
});
// Initialize vector store asynchronously
this.initializeVectorStore().catch((error) => {
this.logger.error("Failed to initialize vector store", error);
});
}
private async initializeVectorStore() {
try {
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.LOADING_DATA);
// 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);
this.vectorStore = new MemoryVectorStore(this.embeddings);
this.isInitialized = true;
return;
}
// Split documents into chunks
const splitDocs = await this.textSplitter.splitDocuments(documents);
this.logger.info(
CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.DOCUMENTS_SPLIT.replace("{totalDocs}", documents.length.toString()).replace(
"{chunks}",
splitDocs.length.toString(),
),
);
// Create vector store from documents
this.vectorStore = await MemoryVectorStore.fromDocuments(splitDocs, this.embeddings);
this.isInitialized = true;
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
this.vectorStore = new MemoryVectorStore(this.embeddings);
this.isInitialized = true;
}
}
private async loadDocuments(): Promise<Document[]> {
const documents: Document[] = [];
try {
// 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 = [
new Document({
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.COMPANY_GUIDE,
metadata: {
type: "company_guide",
category: "guidance",
language: "فارسی",
},
}),
new Document({
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.COMPANY_FAQ,
metadata: {
type: "company_faq",
category: "support",
language: "فارسی",
},
}),
new Document({
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.SEARCH_GUIDE,
metadata: {
type: "search_guide",
category: "tutorial",
language: "فارسی",
},
}),
new Document({
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.INDUSTRY_GUIDE,
metadata: {
type: "industry_guide",
category: "education",
language: "فارسی",
},
}),
];
documents.push(...companyGuidanceDocuments);
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);
return [];
}
}
async generateResponse(message: string, context: IChatContext): Promise<IChatbotResponse> {
if (!this.isInitialized) {
throw new Error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.SERVICE_NOT_INITIALIZED);
}
try {
// Retrieve relevant documents based on the user's query
const relevantDocs = await this.vectorStore.similaritySearch(message, 5);
// Build context from retrieved documents
const retrievedContext = relevantDocs.map((doc, index) => `${index + 1}. ${doc.pageContent}`).join("\n\n");
// Build conversation history
const conversationHistory =
context.conversationHistory
?.slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY / 4)
?.map((msg) => {
const roleLabel = msg.type === "user" ? "کاربر" : msg.type === "bot" ? "ربات" : "سیستم";
return `${roleLabel}: ${msg.content}`;
})
?.join("\n") || "";
// Create the prompt template using constants
const promptTemplate = PromptTemplate.fromTemplate(CHATBOT_CONSTANTS.COMPANY_GUIDANCE_SYSTEM_PROMPT);
// Create the runnable sequence
const chain = RunnableSequence.from([promptTemplate, this.llm, new StringOutputParser()]);
// Execute the chain
const response = await chain.invoke({
context: retrievedContext,
conversation_history: conversationHistory,
question: message,
});
// Calculate approximate token usage
const tokensUsed = this.estimateTokens(retrievedContext) + this.estimateTokens(message) + this.estimateTokens(response);
return {
message: response,
confidence: 0.9, // Higher confidence for company guidance
sources: relevantDocs.map((doc) => {
const type = doc.metadata.type || "شرکت";
return CHATBOT_CONSTANTS.SOURCE_TYPE_LABELS[type as keyof typeof CHATBOT_CONSTANTS.SOURCE_TYPE_LABELS] || type;
}),
tokensUsed,
context: {
model: `${CHATBOT_CONSTANTS.DEFAULT_MODEL}-company-guide`,
relevantDataFound: relevantDocs.length > 0,
documentsRetrieved: relevantDocs.length,
vectorStoreInitialized: this.isInitialized,
language: "فارسی",
dataTypes: relevantDocs.map((doc) => doc.metadata.type).filter((type, index, arr) => arr.indexOf(type) === index),
focus: "company_and_industry_guidance",
},
};
} catch (error) {
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.RESPONSE_ERROR, error);
throw new Error(CHATBOT_CONSTANTS.ERROR_MESSAGES.LLM_SERVICE_ERROR);
}
}
async generateStreamResponse(message: string, context: IChatContext) {
if (!this.isInitialized) {
throw new Error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.SERVICE_NOT_INITIALIZED);
}
try {
// Retrieve relevant documents
const relevantDocs = await this.vectorStore.similaritySearch(message, 5);
const retrievedContext = relevantDocs.map((doc, index) => `${index + 1}. ${doc.pageContent}`).join("\n\n");
// Build conversation history
const conversationHistory =
context.conversationHistory
?.slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY / 4)
?.map((msg) => {
const roleLabel = msg.type === "user" ? "کاربر" : msg.type === "bot" ? "ربات" : "سیستم";
return `${roleLabel}: ${msg.content}`;
})
?.join("\n") || "";
// Create prompt template using constants
const promptTemplate = PromptTemplate.fromTemplate(CHATBOT_CONSTANTS.COMPANY_GUIDANCE_SYSTEM_PROMPT);
// Create streaming chain
const chain = RunnableSequence.from([promptTemplate, this.llm, new StringOutputParser()]);
// Return async iterable for streaming
const stream = await chain.stream({
context: retrievedContext,
conversation_history: conversationHistory,
question: message,
});
return stream;
} catch (error) {
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.STREAM_RESPONSE_ERROR, error);
throw new Error(CHATBOT_CONSTANTS.ERROR_MESSAGES.LLM_SERVICE_ERROR);
}
}
async refreshVectorStore(): Promise<void> {
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.REFRESHING_VECTOR_STORE);
this.isInitialized = false;
await this.initializeVectorStore();
}
private estimateTokens(text: string): number {
// Rough estimation: 1 token ≈ 4 characters for most languages
return Math.ceil(text.length / 4);
}
get initialized(): boolean {
return this.isInitialized;
}
}
@@ -2,7 +2,6 @@ import { inject, injectable } from "inversify";
import { Socket } from "socket.io";
import { WebSocketMessage } from "../../../common/enums/message.enum";
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";