chatbot : step 1
This commit is contained in:
@@ -54,6 +54,13 @@ import { ChatService } from "../modules/chat/chat.service";
|
|||||||
import { ChatRepo, createChatRepo } from "../modules/chat/repository/chat.repository";
|
import { ChatRepo, createChatRepo } from "../modules/chat/repository/chat.repository";
|
||||||
import { ChatMessageRepo, createChatMessageRepo } from "../modules/chat/repository/message.repository";
|
import { ChatMessageRepo, createChatMessageRepo } from "../modules/chat/repository/message.repository";
|
||||||
import { WsAuthService } from "../modules/chat/wsAuth.service";
|
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";
|
||||||
|
import { ChatMessageRepository, createChatMessageRepository } from "../modules/chatbot/repositories/chat-message.repository";
|
||||||
import { ContactUsRepo, CreateContactUsRepo } from "../modules/contact-us/contactUs.repository";
|
import { ContactUsRepo, CreateContactUsRepo } from "../modules/contact-us/contactUs.repository";
|
||||||
import { ContactUsService } from "../modules/contact-us/contactUs.service";
|
import { ContactUsService } from "../modules/contact-us/contactUs.service";
|
||||||
import { CouponRepo, CouponUsageRepo, createCouponRepo, createCouponUsageRepo } from "../modules/Coupon/coupon.repository";
|
import { CouponRepo, CouponUsageRepo, createCouponRepo, createCouponUsageRepo } from "../modules/Coupon/coupon.repository";
|
||||||
@@ -197,6 +204,11 @@ const containerModules = new AsyncContainerModule(async (bind) => {
|
|||||||
bind<LearningService>(IOCTYPES.LearningService).to(LearningService).inSingletonScope();
|
bind<LearningService>(IOCTYPES.LearningService).to(LearningService).inSingletonScope();
|
||||||
bind<RedisService>(IOCTYPES.RedisService).to(RedisService).inSingletonScope();
|
bind<RedisService>(IOCTYPES.RedisService).to(RedisService).inSingletonScope();
|
||||||
bind<CacheService>(IOCTYPES.CacheService).to(CacheService).inSingletonScope();
|
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
|
// #endregion
|
||||||
// #region repository
|
// #region repository
|
||||||
bind<CategoryRepository>(IOCTYPES.CategoryRepository).toDynamicValue(createCategoryRepo).inSingletonScope();
|
bind<CategoryRepository>(IOCTYPES.CategoryRepository).toDynamicValue(createCategoryRepo).inSingletonScope();
|
||||||
@@ -279,6 +291,8 @@ const containerModules = new AsyncContainerModule(async (bind) => {
|
|||||||
bind<AboutUsRepo>(IOCTYPES.AboutUsRepo).toDynamicValue(CreateAboutUsRepo).inSingletonScope();
|
bind<AboutUsRepo>(IOCTYPES.AboutUsRepo).toDynamicValue(CreateAboutUsRepo).inSingletonScope();
|
||||||
bind<SiteSettingRepo>(IOCTYPES.SiteSettingRepo).toDynamicValue(CreateSiteSettingRepo).inSingletonScope();
|
bind<SiteSettingRepo>(IOCTYPES.SiteSettingRepo).toDynamicValue(CreateSiteSettingRepo).inSingletonScope();
|
||||||
bind<NewsletterRepo>(IOCTYPES.NewsletterRepo).toDynamicValue(CreateNewsletterRepo).inSingletonScope();
|
bind<NewsletterRepo>(IOCTYPES.NewsletterRepo).toDynamicValue(CreateNewsletterRepo).inSingletonScope();
|
||||||
|
bind<ChatSessionRepository>(IOCTYPES.ChatSessionRepository).toDynamicValue(createChatSessionRepository).inSingletonScope();
|
||||||
|
bind<ChatMessageRepository>(IOCTYPES.ChatMessageRepository).toDynamicValue(createChatMessageRepository).inSingletonScope();
|
||||||
// #endregion
|
// #endregion
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ export const IOCTYPES = {
|
|||||||
NewsletterService: Symbol.for("NewsletterService"),
|
NewsletterService: Symbol.for("NewsletterService"),
|
||||||
PricingService: Symbol.for("PricingService"),
|
PricingService: Symbol.for("PricingService"),
|
||||||
OrderQueue: Symbol.for("OrderQueue"),
|
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"),
|
||||||
// #endregion
|
// #endregion
|
||||||
// #region repository
|
// #region repository
|
||||||
CategoryRepository: Symbol.for("CategoryRepository"),
|
CategoryRepository: Symbol.for("CategoryRepository"),
|
||||||
@@ -131,6 +137,8 @@ export const IOCTYPES = {
|
|||||||
ContactUsRepo: Symbol.for("ContactUsRepo"),
|
ContactUsRepo: Symbol.for("ContactUsRepo"),
|
||||||
AboutUsRepo: Symbol.for("AboutUsRepo"),
|
AboutUsRepo: Symbol.for("AboutUsRepo"),
|
||||||
NewsletterRepo: Symbol.for("NewsletterRepo"),
|
NewsletterRepo: Symbol.for("NewsletterRepo"),
|
||||||
|
ChatSessionRepository: Symbol.for("ChatSessionRepository"),
|
||||||
|
ChatMessageRepository: Symbol.for("ChatMessageRepository"),
|
||||||
// #endregion
|
// #endregion
|
||||||
Logger: Symbol.for("Logger"),
|
Logger: Symbol.for("Logger"),
|
||||||
ZarinPalGateway: Symbol.for("ZarinPalGateway"),
|
ZarinPalGateway: Symbol.for("ZarinPalGateway"),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import "./modules/blog/blog.controller";
|
|||||||
import "./modules/landing/landing.controller";
|
import "./modules/landing/landing.controller";
|
||||||
import "./modules/ticket/ticket.controller";
|
import "./modules/ticket/ticket.controller";
|
||||||
import "./modules/chat/chat.controller";
|
import "./modules/chat/chat.controller";
|
||||||
|
import "./modules/chatbot/chatbot.controller";
|
||||||
import "./modules/job/job.controller";
|
import "./modules/job/job.controller";
|
||||||
import "./modules/faq/faq.controller";
|
import "./modules/faq/faq.controller";
|
||||||
import "./modules/notification/notification.controller";
|
import "./modules/notification/notification.controller";
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
import { Expose } from "class-transformer";
|
||||||
import { IsOptional, IsString, MaxLength } from "class-validator";
|
import { IsOptional, IsString, MaxLength } from "class-validator";
|
||||||
|
|
||||||
|
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||||
|
|
||||||
export class CreateChatSessionDto {
|
export class CreateChatSessionDto {
|
||||||
@ApiProperty({ description: "Title for the chat session", example: "Help with payment issues" })
|
@Expose()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(255)
|
@MaxLength(255)
|
||||||
|
@ApiProperty({ type: "string", description: "Title for the chat session", example: "Help with payment issues" })
|
||||||
title!: string;
|
title!: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ApiPropertyOptional({ description: "Initial context for the chat", example: { user: "John Doe", orderId: "123456" } })
|
@ApiProperty({ type: "object", description: "Initial context for the chat", example: { user: "John Doe", orderId: "123456" }, required: false })
|
||||||
context?: Record<string, any>;
|
context?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { Expose } from "class-transformer";
|
||||||
import { IsOptional, IsString, IsUUID, MaxLength } from "class-validator";
|
import { IsOptional, IsString, MaxLength } from "class-validator";
|
||||||
|
import { isValidObjectId } from "mongoose";
|
||||||
|
|
||||||
|
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||||
|
import { IsValidId } from "../../../common/decorator/validation.decorator";
|
||||||
|
import { ChatSessionModel } from "../models/chat-session.model";
|
||||||
|
|
||||||
export class SendMessageDto {
|
export class SendMessageDto {
|
||||||
@ApiProperty({ description: "Message content", example: "How can I cancel my subscription?" })
|
@Expose()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(2000)
|
@MaxLength(2000)
|
||||||
|
@ApiProperty({ type: "string", description: "Message content", example: "How can I cancel my subscription?" })
|
||||||
content!: string;
|
content!: string;
|
||||||
|
|
||||||
@ApiProperty({ description: "Chat session ID" })
|
@Expose()
|
||||||
@IsUUID()
|
@IsValidId([ChatSessionModel])
|
||||||
|
@ApiProperty({ type: "string", description: "Chat session ID" })
|
||||||
sessionId!: string;
|
sessionId!: string;
|
||||||
|
|
||||||
@ApiProperty({ description: "ID of message being replied to", required: false })
|
@Expose()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUUID()
|
@IsString()
|
||||||
|
@ApiProperty({ type: "string", description: "ID of message being replied to", required: false })
|
||||||
responseToId?: string;
|
responseToId?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: "Additional metadata for the message", required: false })
|
@Expose()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ApiProperty({ type: "object", description: "Additional metadata for the message", required: false })
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { Expose } from "class-transformer";
|
||||||
import { IsNotEmpty, IsUUID } from "class-validator";
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
import { isValidObjectId } from "mongoose";
|
||||||
|
|
||||||
|
import { ApiProperty } from "../../../common/decorator/swggerDocs";
|
||||||
import { CommonMessage } from "../../../common/enums/message.enum";
|
import { CommonMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { IsValidId } from "../../../common/decorator/validation.decorator";
|
||||||
|
import { ChatSessionModel } from "../models/chat-session.model";
|
||||||
|
|
||||||
export class SessionIdParamDto {
|
export class SessionIdParamDto {
|
||||||
@IsNotEmpty({ message: CommonMessage.SESSION_ID_REQUIRED })
|
@Expose()
|
||||||
@IsUUID("7", { message: CommonMessage.SESSION_ID_SHOULD_BE_UUID })
|
@IsNotEmpty({ message: CommonMessage.SESSION_ID_REQUIRED || "Session ID is required" })
|
||||||
@ApiProperty({ description: "Session id of the entity", example: "8b1e8b1e-8b1e-8b1e-8b1e-8b1e8b1e8b1e" })
|
@IsValidId([ChatSessionModel])
|
||||||
|
@ApiProperty({ type: "string", description: "Session id of the entity", example: "66eff8c0c6ad5530b996c432" })
|
||||||
sessionId!: string;
|
sessionId!: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,148 +1,149 @@
|
|||||||
import { Body, Controller, Get, Param, Post, Put, Query, Res } from "@nestjs/common";
|
import { Request, Response } from "express";
|
||||||
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
|
import { inject } from "inversify";
|
||||||
import { FastifyReply } from "fastify";
|
import { controller, httpGet, httpPost, httpPut, queryParam, request, requestBody, requestParam, response } from "inversify-express-utils";
|
||||||
|
|
||||||
import { CreateChatSessionDto } from "./DTO/create-chat-session.dto";
|
import { CreateChatSessionDto } from "./DTO/create-chat-session.dto";
|
||||||
import { SendMessageDto } from "./DTO/send-message.dto";
|
import { SendMessageDto } from "./DTO/send-message.dto";
|
||||||
import { SessionIdParamDto } from "./DTO/session-id.param.dto";
|
import { SessionIdParamDto } from "./DTO/session-id.param.dto";
|
||||||
import { ChatbotService } from "./providers/chatbot.service";
|
import { ChatbotService } from "./providers/chatbot.service";
|
||||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
import { BaseController } from "../../common/base/controller";
|
||||||
import { UserDec } from "../../common/decorators/user.decorator";
|
import { ApiAuth, ApiModel, ApiOperation, ApiParam, ApiResponse, ApiTags } from "../../common/decorator/swggerDocs";
|
||||||
import { PaginationDto } from "../../common/DTO/pagination.dto";
|
import { PaginationDTO } from "../../common/dto/pagination.dto";
|
||||||
|
import { HttpStatus } from "../../common/enums/httpStatus.enum";
|
||||||
|
import { Guard } from "../../core/middlewares/guard.middleware";
|
||||||
|
import { ValidationMiddleware } from "../../core/middlewares/validator.middleware";
|
||||||
|
import { IOCTYPES } from "../../IOC/ioc.types";
|
||||||
|
import { IUser } from "../user/models/Abstraction/IUser";
|
||||||
|
|
||||||
@Controller("chatbot")
|
@controller("/chatbot")
|
||||||
@AuthGuards()
|
@ApiTags("Chatbot")
|
||||||
export class ChatbotController {
|
class ChatbotController extends BaseController {
|
||||||
constructor(private chatbotService: ChatbotService) {}
|
@inject(IOCTYPES.ChatbotService) private chatbotService: ChatbotService;
|
||||||
|
|
||||||
@Post("sessions")
|
@ApiOperation("Create a new chat session")
|
||||||
@ApiOperation({ summary: "Create a new chat session" })
|
@ApiResponse("Chat session created successfully", HttpStatus.Created)
|
||||||
@ApiResponse({ status: 201, description: "Chat session created successfully" })
|
@ApiModel(CreateChatSessionDto)
|
||||||
createSession(@UserDec("id") userId: string, @Body() createDto: CreateChatSessionDto) {
|
@ApiAuth()
|
||||||
return this.chatbotService.createChatSession(userId, createDto);
|
@httpPost("/sessions", Guard.authUser(), ValidationMiddleware.validateInput(CreateChatSessionDto))
|
||||||
|
public async createSession(@request() req: Request, @requestBody() createDto: CreateChatSessionDto) {
|
||||||
|
const user = req.user as IUser;
|
||||||
|
const data = await this.chatbotService.createChatSession(user._id.toString(), createDto);
|
||||||
|
return this.response({ data }, HttpStatus.Created);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("sessions")
|
@ApiOperation("Get user's chat sessions")
|
||||||
@ApiOperation({ summary: "Get user's chat sessions" })
|
@ApiResponse("Chat sessions retrieved successfully")
|
||||||
@ApiResponse({ status: 200, description: "Chat sessions retrieved successfully" })
|
@ApiAuth()
|
||||||
getUserSessions(@UserDec("id") userId: string, @Query() queryDto: PaginationDto) {
|
@httpGet("/sessions", Guard.authUser(), ValidationMiddleware.validateQuery(PaginationDTO))
|
||||||
return this.chatbotService.getUserChatSessions(userId, queryDto.limit);
|
public async getUserSessions(@request() req: Request, @queryParam() queryDto: PaginationDTO) {
|
||||||
|
const user = req.user as IUser;
|
||||||
|
const limit = queryDto.limit || 10;
|
||||||
|
const data = await this.chatbotService.getUserChatSessions(user._id.toString(), limit);
|
||||||
|
return this.response({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("sessions/:sessionId")
|
@ApiOperation("Get a specific chat session with messages")
|
||||||
@ApiOperation({ summary: "Get a specific chat session with messages" })
|
@ApiResponse("Chat session retrieved successfully")
|
||||||
@ApiResponse({ status: 200, description: "Chat session retrieved successfully" })
|
@ApiParam("sessionId", "Session ID", true)
|
||||||
getSession(@UserDec("id") userId: string, @Param() param: SessionIdParamDto) {
|
@ApiAuth()
|
||||||
return this.chatbotService.getChatSession(param.sessionId, userId);
|
@httpGet("/sessions/:sessionId", Guard.authUser(), ValidationMiddleware.validateParameter(SessionIdParamDto))
|
||||||
|
public async getSession(@request() req: Request, @requestParam() param: SessionIdParamDto) {
|
||||||
|
const user = req.user as IUser;
|
||||||
|
const data = await this.chatbotService.getChatSession(param.sessionId, user._id.toString());
|
||||||
|
return this.response({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("messages")
|
@ApiOperation("Send a message in a chat session (Enhanced with LangChain + Fallback)")
|
||||||
@ApiOperation({ summary: "Send a message in a chat session (Enhanced with LangChain + Fallback)" })
|
@ApiResponse("Message sent successfully with enhanced AI", HttpStatus.Created)
|
||||||
@ApiResponse({ status: 201, description: "Message sent successfully with enhanced AI" })
|
@ApiModel(SendMessageDto)
|
||||||
sendMessage(@UserDec("id") userId: string, @Body() sendDto: SendMessageDto) {
|
@ApiAuth()
|
||||||
return this.chatbotService.sendMessage(userId, sendDto);
|
@httpPost("/messages", Guard.authUser(), ValidationMiddleware.validateInput(SendMessageDto))
|
||||||
|
public async sendMessage(@request() req: Request, @requestBody() sendDto: SendMessageDto) {
|
||||||
|
const user = req.user as IUser;
|
||||||
|
const data = await this.chatbotService.sendMessage(user._id.toString(), sendDto);
|
||||||
|
return this.response({ data }, HttpStatus.Created);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("messages/stream")
|
@ApiOperation("Send a message and get streaming response (Enhanced with LangChain + Fallback)")
|
||||||
@ApiOperation({ summary: "Send a message and get streaming response (Enhanced with LangChain + Fallback)" })
|
@ApiResponse("Enhanced streaming response initiated")
|
||||||
@ApiResponse({ status: 200, description: "Enhanced streaming response initiated" })
|
@ApiModel(SendMessageDto)
|
||||||
async sendMessageStream(@UserDec("id") userId: string, @Body() sendDto: SendMessageDto, @Res() res: FastifyReply): Promise<void> {
|
@ApiAuth()
|
||||||
|
@httpPost("/messages/stream", Guard.authUser(), ValidationMiddleware.validateInput(SendMessageDto))
|
||||||
|
public async sendMessageStream(
|
||||||
|
@request() req: Request,
|
||||||
|
@requestBody() sendDto: SendMessageDto,
|
||||||
|
@response() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const user = req.user as IUser;
|
||||||
try {
|
try {
|
||||||
// Now uses LangChain by default with automatic Gemini fallback
|
// Now uses LangChain by default with automatic Gemini fallback
|
||||||
const { userMessage, streamGenerator } = await this.chatbotService.sendMessageStream(userId, sendDto);
|
const { userMessage, streamGenerator } = await this.chatbotService.sendMessageStream(user._id.toString(), sendDto);
|
||||||
|
|
||||||
// Set headers for Server-Sent Events
|
// Set headers for Server-Sent Events
|
||||||
res.header("Content-Type", "text/event-stream");
|
res.setHeader("Content-Type", "text/event-stream");
|
||||||
res.header("Cache-Control", "no-cache");
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
res.header("Connection", "keep-alive");
|
res.setHeader("Connection", "keep-alive");
|
||||||
res.header("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
// Send initial user message
|
// Send initial user message
|
||||||
res.raw.write(`data: ${JSON.stringify({ type: "user_message", data: userMessage, enhanced: true })}\n\n`);
|
res.write(`data: ${JSON.stringify({ type: "user_message", data: userMessage, enhanced: true })}\n\n`);
|
||||||
|
|
||||||
// Start streaming bot response
|
// Start streaming bot response
|
||||||
res.raw.write(`data: ${JSON.stringify({ type: "bot_response_start", provider: "enhanced" })}\n\n`);
|
res.write(`data: ${JSON.stringify({ type: "bot_response_start", provider: "enhanced" })}\n\n`);
|
||||||
|
|
||||||
const stream = await streamGenerator();
|
const stream = await streamGenerator();
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
if (chunk) {
|
if (chunk) {
|
||||||
res.raw.write(`data: ${JSON.stringify({ type: "bot_response_chunk", data: chunk })}\n\n`);
|
res.write(`data: ${JSON.stringify({ type: "bot_response_chunk", data: chunk })}\n\n`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// End streaming
|
// End streaming
|
||||||
res.raw.write(`data: ${JSON.stringify({ type: "bot_response_end" })}\n\n`);
|
res.write(`data: ${JSON.stringify({ type: "bot_response_end" })}\n\n`);
|
||||||
res.raw.end();
|
res.end();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.raw.write(
|
res.write(
|
||||||
`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ هوشمند. LangChain با خطا مواجه شد و به Gemini بازگشت ⚠️" })}\n\n`,
|
`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ هوشمند. LangChain با خطا مواجه شد و به Gemini بازگشت ⚠️" })}\n\n`,
|
||||||
);
|
);
|
||||||
res.raw.end();
|
res.end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post("training/refresh")
|
@ApiOperation("Refresh LangChain training data from database")
|
||||||
@ApiOperation({ summary: "Refresh LangChain training data from database" })
|
@ApiResponse("Training data refreshed successfully")
|
||||||
@ApiResponse({ status: 200, description: "Training data refreshed successfully" })
|
@ApiAuth()
|
||||||
refreshTrainingData(@UserDec("id") _userId: string) {
|
@httpPost("/training/refresh", Guard.authUser())
|
||||||
return this.chatbotService.refreshLangChainData();
|
public async refreshTrainingData(@request() _req: Request) {
|
||||||
|
const data = await this.chatbotService.refreshLangChainData();
|
||||||
|
return this.response({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("sessions/:sessionId/close")
|
@ApiOperation("Close a chat session")
|
||||||
@ApiOperation({ summary: "Close a chat session" })
|
@ApiResponse("Chat session closed successfully")
|
||||||
@ApiResponse({ status: 200, description: "Chat session closed successfully" })
|
@ApiParam("sessionId", "Session ID", true)
|
||||||
closeSession(@UserDec("id") userId: string, @Param() param: SessionIdParamDto) {
|
@ApiAuth()
|
||||||
return this.chatbotService.closeChatSession(param.sessionId, userId);
|
@httpPut("/sessions/:sessionId/close", Guard.authUser(), ValidationMiddleware.validateParameter(SessionIdParamDto))
|
||||||
|
public async closeSession(@request() req: Request, @requestParam() param: SessionIdParamDto) {
|
||||||
|
const user = req.user as IUser;
|
||||||
|
const data = await this.chatbotService.closeChatSession(param.sessionId, user._id.toString());
|
||||||
|
return this.response({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put("sessions/:sessionId/messages/read")
|
@ApiOperation("Mark messages as read")
|
||||||
@ApiOperation({ summary: "Mark messages as read" })
|
@ApiResponse("Messages marked as read successfully")
|
||||||
@ApiResponse({ status: 200, description: "Messages marked as read successfully" })
|
@ApiParam("sessionId", "Session ID", true)
|
||||||
markMessagesAsRead(@UserDec("id") userId: string, @Param() param: SessionIdParamDto, @Body() body: { messageIds: string[] }) {
|
@ApiAuth()
|
||||||
return this.chatbotService.markMessagesAsRead(param.sessionId, userId, body.messageIds);
|
@httpPut("/sessions/:sessionId/messages/read", Guard.authUser(), ValidationMiddleware.validateParameter(SessionIdParamDto))
|
||||||
|
public async markMessagesAsRead(
|
||||||
|
@request() req: Request,
|
||||||
|
@requestParam() param: SessionIdParamDto,
|
||||||
|
@requestBody() body: { messageIds: string[] },
|
||||||
|
) {
|
||||||
|
const user = req.user as IUser;
|
||||||
|
const data = await this.chatbotService.markMessagesAsRead(param.sessionId, user._id.toString(), body.messageIds);
|
||||||
|
return this.response({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Post("messages/langchain")
|
|
||||||
// @ApiOperation({ summary: "Send a message using LangChain with enhanced RAG capabilities" })
|
|
||||||
// @ApiResponse({ status: 201, description: "Message sent successfully with LangChain", type: ChatMessageResponseDto })
|
|
||||||
// sendMessageWithLangChain(@UserDec("id") userId: string, @Body() sendDto: SendMessageDto): Promise<ChatMessageResponseDto> {
|
|
||||||
// return this.chatbotService.sendMessageWithLangChain(userId, sendDto);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @Post("messages/langchain/stream")
|
|
||||||
// @ApiOperation({ summary: "Send a message and get streaming response using LangChain" })
|
|
||||||
// @ApiResponse({ status: 200, description: "Streaming response initiated with LangChain" })
|
|
||||||
// async sendMessageStreamWithLangChain(@UserDec("id") userId: string, @Body() sendDto: SendMessageDto, @Res() res: FastifyReply): Promise<void> {
|
|
||||||
// try {
|
|
||||||
// const { userMessage, streamGenerator } = await this.chatbotService.sendMessageStreamWithLangChain(userId, sendDto);
|
|
||||||
|
|
||||||
// // Set headers for Server-Sent Events
|
|
||||||
// res.header("Content-Type", "text/event-stream");
|
|
||||||
// res.header("Cache-Control", "no-cache");
|
|
||||||
// res.header("Connection", "keep-alive");
|
|
||||||
// res.header("Access-Control-Allow-Origin", "*");
|
|
||||||
|
|
||||||
// // Send initial user message
|
|
||||||
// res.raw.write(`data: ${JSON.stringify({ type: "user_message", data: userMessage })}\n\n`);
|
|
||||||
|
|
||||||
// // Start streaming bot response
|
|
||||||
// res.raw.write(`data: ${JSON.stringify({ type: "bot_response_start", provider: "langchain" })}\n\n`);
|
|
||||||
|
|
||||||
// const stream = await streamGenerator();
|
|
||||||
// for await (const chunk of stream) {
|
|
||||||
// if (chunk) {
|
|
||||||
// res.raw.write(`data: ${JSON.stringify({ type: "bot_response_chunk", data: chunk })}\n\n`);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // End streaming
|
|
||||||
// res.raw.write(`data: ${JSON.stringify({ type: "bot_response_end" })}\n\n`);
|
|
||||||
// res.raw.end();
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error(error);
|
|
||||||
// res.raw.write(`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ با LangChain. لطفاً دوباره تلاش کنید. ⚠️" })}\n\n`);
|
|
||||||
// res.raw.end();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { ChatbotController };
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
|
export enum MessageType {
|
||||||
|
USER = "user",
|
||||||
|
BOT = "bot",
|
||||||
|
SYSTEM = "system",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MessageStatus {
|
||||||
|
SENT = "sent",
|
||||||
|
DELIVERED = "delivered",
|
||||||
|
READ = "read",
|
||||||
|
FAILED = "failed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChatMessage {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
content: string;
|
||||||
|
type: MessageType;
|
||||||
|
status: MessageStatus;
|
||||||
|
session: Types.ObjectId;
|
||||||
|
sender?: Types.ObjectId;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
responseToId?: string;
|
||||||
|
tokensUsed: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
|
export enum ChatSessionStatus {
|
||||||
|
ACTIVE = "active",
|
||||||
|
CLOSED = "closed",
|
||||||
|
ARCHIVED = "archived",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChatSession {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
title: string;
|
||||||
|
user: Types.ObjectId;
|
||||||
|
status: ChatSessionStatus;
|
||||||
|
context?: Record<string, any>;
|
||||||
|
lastMessageAt?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
|
import { IChatMessage, MessageStatus, MessageType } from "./Abstraction/IChatMessage";
|
||||||
|
|
||||||
|
const chatMessageSchema = new Schema<IChatMessage>(
|
||||||
|
{
|
||||||
|
content: { type: String, required: true },
|
||||||
|
type: { type: String, enum: MessageType, required: true },
|
||||||
|
status: { type: String, enum: MessageStatus, default: MessageStatus.SENT },
|
||||||
|
session: { type: Schema.Types.ObjectId, ref: "ChatSession", required: true },
|
||||||
|
sender: { type: Schema.Types.ObjectId, ref: "User", default: null },
|
||||||
|
metadata: { type: Schema.Types.Mixed, default: null },
|
||||||
|
responseToId: { type: String, default: null },
|
||||||
|
tokensUsed: { type: Number, default: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
toJSON: { virtuals: true, versionKey: false },
|
||||||
|
id: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
chatMessageSchema.index({ session: 1, createdAt: -1 });
|
||||||
|
chatMessageSchema.index({ session: 1, createdAt: 1 });
|
||||||
|
|
||||||
|
const ChatMessageModel = model<IChatMessage>("ChatMessage", chatMessageSchema);
|
||||||
|
|
||||||
|
export { ChatMessageModel, MessageStatus, MessageType };
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
|
import { ChatSessionStatus, IChatSession } from "./Abstraction/IChatSession";
|
||||||
|
|
||||||
|
const chatSessionSchema = new Schema<IChatSession>(
|
||||||
|
{
|
||||||
|
title: { type: String, required: true, maxlength: 255 },
|
||||||
|
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
|
||||||
|
status: { type: String, enum: ChatSessionStatus, default: ChatSessionStatus.ACTIVE },
|
||||||
|
context: { type: Schema.Types.Mixed, default: null },
|
||||||
|
lastMessageAt: { type: Date, default: null },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
toJSON: { virtuals: true, versionKey: false },
|
||||||
|
id: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
chatSessionSchema.index({ user: 1, lastMessageAt: -1 });
|
||||||
|
chatSessionSchema.index({ status: 1 });
|
||||||
|
|
||||||
|
const ChatSessionModel = model<IChatSession>("ChatSession", chatSessionSchema);
|
||||||
|
|
||||||
|
export { ChatSessionModel, ChatSessionStatus };
|
||||||
|
|
||||||
@@ -1,80 +1,86 @@
|
|||||||
import { EntityManager } from "@mikro-orm/postgresql";
|
import { inject, injectable } from "inversify";
|
||||||
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
import { isValidObjectId } from "mongoose";
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
|
|
||||||
import { LangChainService } from "./langchain.service";
|
import { LangChainService } from "./langchain.service";
|
||||||
import { LLMService } from "./llm.service";
|
import { LLMService } from "./llm.service";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { UserModel } from "../../user/models/user.model";
|
||||||
// import { UsersService } from "../../users/services/users.service";
|
|
||||||
import { CreateChatSessionDto } from "../DTO/create-chat-session.dto";
|
import { CreateChatSessionDto } from "../DTO/create-chat-session.dto";
|
||||||
import { SendMessageDto } from "../DTO/send-message.dto";
|
import { SendMessageDto } from "../DTO/send-message.dto";
|
||||||
import { ChatMessage, MessageStatus, MessageType } from "../entities/chat-message.entity";
|
import { ChatMessageModel, MessageStatus, MessageType } from "../models/chat-message.model";
|
||||||
import { ChatSession, ChatSessionStatus } from "../entities/chat-session.entity";
|
import { ChatSessionModel, ChatSessionStatus } from "../models/chat-session.model";
|
||||||
import { IChatContext } from "../interfaces/chatbot.interface";
|
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 { ChatMessageRepository } from "../repositories/chat-message.repository";
|
||||||
import { ChatSessionRepository } from "../repositories/chat-session.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 {
|
export enum LLMProvider {
|
||||||
LANGCHAIN = "langchain",
|
LANGCHAIN = "langchain",
|
||||||
GEMINI = "gemini",
|
GEMINI = "gemini",
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@injectable()
|
||||||
export class ChatbotService {
|
export class ChatbotService {
|
||||||
private readonly logger = new Logger(ChatbotService.name);
|
private readonly logger: Logger;
|
||||||
private defaultProvider: LLMProvider;
|
private defaultProvider: LLMProvider;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private em: EntityManager,
|
@inject(IOCTYPES.ChatbotLLMService) private llmService: LLMService,
|
||||||
private llmService: LLMService,
|
@inject(IOCTYPES.ChatbotLangChainService) private langChainService: LangChainService,
|
||||||
private langChainService: LangChainService,
|
@inject(IOCTYPES.ChatSessionRepository) private chatSessionRepo: ChatSessionRepository,
|
||||||
private chatSessionRepo: ChatSessionRepository,
|
@inject(IOCTYPES.ChatMessageRepository) private chatMessageRepo: ChatMessageRepository,
|
||||||
private chatMessageRepo: ChatMessageRepository,
|
|
||||||
private configService: ConfigService,
|
|
||||||
// private usersService: UsersService,
|
|
||||||
) {
|
) {
|
||||||
|
this.logger = new Logger("ChatbotService");
|
||||||
// Configure default provider - LangChain for enhanced responses
|
// Configure default provider - LangChain for enhanced responses
|
||||||
this.defaultProvider = this.configService.get("DEFAULT_LLM_PROVIDER") === "gemini" ? LLMProvider.GEMINI : LLMProvider.LANGCHAIN;
|
this.defaultProvider = process.env.DEFAULT_LLM_PROVIDER === "gemini" ? LLMProvider.GEMINI : LLMProvider.LANGCHAIN;
|
||||||
|
this.logger.info(`Using ${this.defaultProvider} as default LLM provider`);
|
||||||
this.logger.log(`Using ${this.defaultProvider} as default LLM provider`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createChatSession(userId: string, createDto: CreateChatSessionDto) {
|
async createChatSession(userId: string, createDto: CreateChatSessionDto) {
|
||||||
const em = this.em.fork();
|
if (!isValidObjectId(userId)) {
|
||||||
const user = await em.findOne(User, { id: userId });
|
throw new BadRequestError("Invalid user ID");
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException("User not found");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = new ChatSession();
|
const user = await UserModel.findById(userId);
|
||||||
session.title = createDto.title;
|
if (!user) {
|
||||||
session.user = user;
|
throw new NotFoundError("User not found");
|
||||||
session.context = createDto.context || {};
|
}
|
||||||
session.lastMessageAt = new Date();
|
|
||||||
|
|
||||||
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);
|
return this.mapSessionToDto(session);
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
async getUserChatSessions(userId: string, limit = 10) {
|
async getUserChatSessions(userId: string, limit = 10) {
|
||||||
const em = this.em.fork();
|
if (!isValidObjectId(userId)) {
|
||||||
const sessions = await this.chatSessionRepo.findByUserWithMessages(userId, limit, em);
|
throw new BadRequestError("Invalid user ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessions = await this.chatSessionRepo.findByUserWithMessages(userId, limit);
|
||||||
return sessions.map((session) => this.mapSessionToDto(session));
|
return sessions.map((session) => this.mapSessionToDto(session));
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
async getChatSession(sessionId: string, userId: string) {
|
async getChatSession(sessionId: string, userId: string) {
|
||||||
const em = this.em.fork();
|
if (!isValidObjectId(sessionId) || !isValidObjectId(userId)) {
|
||||||
const session = await em.findOne(ChatSession, { id: sessionId, user: userId }, { populate: ["messages", "messages.sender"] });
|
throw new BadRequestError("Invalid session or user ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await this.chatSessionRepo.findByUserAndId(sessionId, userId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new NotFoundException("Chat session not found");
|
throw new NotFoundError("Chat session not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.mapSessionToDto(session);
|
return this.mapSessionToDto(session);
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
async sendMessage(userId: string, sendDto: SendMessageDto, provider?: LLMProvider) {
|
async sendMessage(userId: string, sendDto: SendMessageDto, provider?: LLMProvider) {
|
||||||
const selectedProvider = provider || this.defaultProvider;
|
const selectedProvider = provider || this.defaultProvider;
|
||||||
@@ -93,47 +99,44 @@ export class ChatbotService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
private async sendMessageWithGemini(userId: string, sendDto: SendMessageDto) {
|
private async sendMessageWithGemini(userId: string, sendDto: SendMessageDto) {
|
||||||
const em = this.em.fork();
|
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
|
||||||
const session = await em.findOne(ChatSession, { id: sendDto.sessionId, user: userId }, { populate: ["messages"] });
|
throw new BadRequestError("Invalid session or user ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await ChatSessionModel.findOne({ _id: sendDto.sessionId, user: userId }).populate("messages").lean();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new NotFoundException("Chat session not found");
|
throw new NotFoundError("Chat session not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.status !== ChatSessionStatus.ACTIVE) {
|
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) {
|
if (!user) {
|
||||||
throw new NotFoundException("User not found");
|
throw new NotFoundError("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user message
|
// Create user message
|
||||||
const userMessage = new ChatMessage();
|
const userMessage = await ChatMessageModel.create({
|
||||||
userMessage.content = sendDto.content;
|
content: sendDto.content,
|
||||||
userMessage.type = MessageType.USER;
|
type: MessageType.USER,
|
||||||
userMessage.session = session;
|
session: sendDto.sessionId,
|
||||||
userMessage.sender = user;
|
sender: userId,
|
||||||
userMessage.responseToId = sendDto.responseToId;
|
responseToId: sendDto.responseToId,
|
||||||
userMessage.metadata = sendDto.metadata;
|
metadata: sendDto.metadata,
|
||||||
|
});
|
||||||
await em.persistAndFlush(userMessage);
|
|
||||||
|
|
||||||
// Update session last message time
|
// 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
|
// 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);
|
return this.mapMessageToDto(userMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
async sendMessageStream(userId: string, sendDto: SendMessageDto, provider?: LLMProvider) {
|
async sendMessageStream(userId: string, sendDto: SendMessageDto, provider?: LLMProvider) {
|
||||||
const selectedProvider = provider || this.defaultProvider;
|
const selectedProvider = provider || this.defaultProvider;
|
||||||
|
|
||||||
@@ -151,46 +154,45 @@ export class ChatbotService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
private async sendMessageStreamWithGemini(userId: string, sendDto: SendMessageDto) {
|
private async sendMessageStreamWithGemini(userId: string, sendDto: SendMessageDto) {
|
||||||
const em = this.em.fork();
|
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
|
||||||
const session = await em.findOne(ChatSession, { id: sendDto.sessionId, user: userId }, { populate: ["messages"] });
|
throw new BadRequestError("Invalid session or user ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await ChatSessionModel.findOne({ _id: sendDto.sessionId, user: userId }).lean();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new NotFoundException("Chat session not found");
|
throw new NotFoundError("Chat session not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.status !== ChatSessionStatus.ACTIVE) {
|
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) {
|
if (!user) {
|
||||||
throw new NotFoundException("User not found");
|
throw new NotFoundError("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user message
|
// Create user message
|
||||||
const userMessage = new ChatMessage();
|
const userMessage = await ChatMessageModel.create({
|
||||||
userMessage.content = sendDto.content;
|
content: sendDto.content,
|
||||||
userMessage.type = MessageType.USER;
|
type: MessageType.USER,
|
||||||
userMessage.session = session;
|
session: sendDto.sessionId,
|
||||||
userMessage.sender = user;
|
sender: userId,
|
||||||
userMessage.responseToId = sendDto.responseToId;
|
responseToId: sendDto.responseToId,
|
||||||
userMessage.metadata = sendDto.metadata;
|
metadata: sendDto.metadata,
|
||||||
|
});
|
||||||
await em.persistAndFlush(userMessage);
|
|
||||||
|
|
||||||
// Update session last message time
|
// Update session last message time
|
||||||
await this.chatSessionRepo.updateLastMessageTime(session.id, em);
|
await this.chatSessionRepo.updateLastMessageTime(sendDto.sessionId);
|
||||||
|
|
||||||
// Get conversation history for context
|
// 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
|
// Build context for LLM
|
||||||
const context: IChatContext = {
|
const context: IChatContext = {
|
||||||
userId,
|
userId,
|
||||||
sessionId: session.id,
|
sessionId: sendDto.sessionId,
|
||||||
conversationHistory: history.reverse().map((msg) => ({
|
conversationHistory: history.reverse().map((msg) => ({
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
type: msg.type as "user" | "bot" | "system",
|
type: msg.type as "user" | "bot" | "system",
|
||||||
@@ -209,18 +211,20 @@ export class ChatbotService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
private async generateBotResponse(sessionId: string, userMessageId: string, userId: string): Promise<void> {
|
||||||
|
|
||||||
private async generateBotResponse(session: ChatSession, userMessage: ChatMessage, userId: string): Promise<void> {
|
|
||||||
const em = this.em.fork();
|
|
||||||
try {
|
try {
|
||||||
// Get conversation history
|
// 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
|
// Build context for LLM
|
||||||
const context: IChatContext = {
|
const context: IChatContext = {
|
||||||
userId,
|
userId,
|
||||||
sessionId: session.id,
|
sessionId,
|
||||||
conversationHistory: history.reverse().map((msg) => ({
|
conversationHistory: history.reverse().map((msg) => ({
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
type: msg.type as "user" | "bot" | "system",
|
type: msg.type as "user" | "bot" | "system",
|
||||||
@@ -230,182 +234,183 @@ export class ChatbotService {
|
|||||||
userPreferences: session.context,
|
userPreferences: session.context,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const userMessage = await ChatMessageModel.findById(userMessageId);
|
||||||
|
if (!userMessage) {
|
||||||
|
throw new NotFoundError("User message not found");
|
||||||
|
}
|
||||||
|
|
||||||
// Generate response using LLM
|
// Generate response using LLM
|
||||||
const llmResponse = await this.llmService.generateResponse(userMessage.content, context);
|
const llmResponse = await this.llmService.generateResponse(userMessage.content, context);
|
||||||
|
|
||||||
// Create bot message
|
// Create bot message
|
||||||
const botMessage = new ChatMessage();
|
const botMessage = await ChatMessageModel.create({
|
||||||
botMessage.content = llmResponse.message;
|
content: llmResponse.message,
|
||||||
botMessage.type = MessageType.BOT;
|
type: MessageType.BOT,
|
||||||
botMessage.session = session;
|
session: sessionId,
|
||||||
botMessage.responseToId = userMessage.id;
|
responseToId: userMessageId,
|
||||||
botMessage.tokensUsed = llmResponse.tokensUsed;
|
tokensUsed: llmResponse.tokensUsed,
|
||||||
botMessage.metadata = {
|
metadata: {
|
||||||
confidence: llmResponse.confidence,
|
confidence: llmResponse.confidence,
|
||||||
sources: llmResponse.sources,
|
sources: llmResponse.sources,
|
||||||
llmContext: llmResponse.context,
|
llmContext: llmResponse.context,
|
||||||
};
|
},
|
||||||
|
});
|
||||||
await em.persistAndFlush(botMessage);
|
|
||||||
|
|
||||||
// Update session last message time
|
// 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) {
|
} 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
|
// Create error message
|
||||||
const errorMessage = new ChatMessage();
|
await ChatMessageModel.create({
|
||||||
errorMessage.content = "متأسفم، در حال حاضر مشکلی در پاسخگویی دارم. لطفاً دوباره تلاش کنید. 🙏";
|
content: "متأسفم، در حال حاضر مشکلی در پاسخگویی دارم. لطفاً دوباره تلاش کنید. 🙏",
|
||||||
errorMessage.type = MessageType.BOT;
|
type: MessageType.BOT,
|
||||||
errorMessage.session = session;
|
session: sessionId,
|
||||||
errorMessage.responseToId = userMessage.id;
|
responseToId: userMessageId,
|
||||||
errorMessage.status = MessageStatus.FAILED;
|
status: MessageStatus.FAILED,
|
||||||
|
});
|
||||||
await em.persistAndFlush(errorMessage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
async closeChatSession(sessionId: string, userId: string) {
|
async closeChatSession(sessionId: string, userId: string) {
|
||||||
const em = this.em.fork();
|
if (!isValidObjectId(sessionId) || !isValidObjectId(userId)) {
|
||||||
const session = await em.findOne(ChatSession, { id: sessionId, user: userId });
|
throw new BadRequestError("Invalid session or user ID");
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
throw new NotFoundException("Chat session not found");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session.status = ChatSessionStatus.CLOSED;
|
const session = await ChatSessionModel.findOneAndUpdate(
|
||||||
await em.persistAndFlush(session);
|
{ _id: sessionId, user: userId },
|
||||||
|
{ status: ChatSessionStatus.CLOSED },
|
||||||
|
{ new: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new NotFoundError("Chat session not found");
|
||||||
|
}
|
||||||
|
|
||||||
return { message: "Chat session closed successfully" };
|
return { message: "Chat session closed successfully" };
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
async markMessagesAsRead(sessionId: string, userId: string, messageIds: string[]) {
|
async markMessagesAsRead(sessionId: string, userId: string, messageIds: string[]) {
|
||||||
const em = this.em.fork();
|
if (!isValidObjectId(sessionId) || !isValidObjectId(userId)) {
|
||||||
const session = await em.findOne(ChatSession, { id: sessionId, user: userId });
|
throw new BadRequestError("Invalid session or user ID");
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
throw new NotFoundException("Chat session not found");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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" };
|
return { message: "Messages marked as read successfully" };
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
private mapSessionToDto(session: IChatSession | any) {
|
||||||
|
const sessionObj = session.toObject ? session.toObject() : session;
|
||||||
private mapSessionToDto(session: ChatSession) {
|
|
||||||
return {
|
return {
|
||||||
id: session.id,
|
id: sessionObj._id.toString(),
|
||||||
title: session.title,
|
title: sessionObj.title,
|
||||||
status: session.status,
|
status: sessionObj.status,
|
||||||
createdAt: session.createdAt,
|
createdAt: sessionObj.createdAt,
|
||||||
lastMessageAt: session.lastMessageAt,
|
lastMessageAt: sessionObj.lastMessageAt,
|
||||||
context: session.context,
|
context: sessionObj.context,
|
||||||
messages: session.messages ? session.messages.getItems().map((msg) => this.mapMessageToDto(msg)) : [],
|
messages: sessionObj.messages ? sessionObj.messages.map((msg: any) => this.mapMessageToDto(msg)) : [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
private mapMessageToDto(message: IChatMessage | any) {
|
||||||
|
const messageObj = message.toObject ? message.toObject() : message;
|
||||||
private mapMessageToDto(message: ChatMessage) {
|
|
||||||
return {
|
return {
|
||||||
id: message.id,
|
id: messageObj._id.toString(),
|
||||||
content: message.content,
|
content: messageObj.content,
|
||||||
type: message.type,
|
type: messageObj.type,
|
||||||
status: message.status,
|
status: messageObj.status,
|
||||||
createdAt: message.createdAt,
|
createdAt: messageObj.createdAt,
|
||||||
responseToId: message.responseToId,
|
responseToId: messageObj.responseToId,
|
||||||
metadata: message.metadata,
|
metadata: messageObj.metadata,
|
||||||
tokensUsed: message.tokensUsed,
|
tokensUsed: messageObj.tokensUsed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
async sendMessageWithLangChain(userId: string, sendDto: SendMessageDto) {
|
async sendMessageWithLangChain(userId: string, sendDto: SendMessageDto) {
|
||||||
const em = this.em.fork();
|
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
|
||||||
const session = await em.findOne(ChatSession, { id: sendDto.sessionId, user: userId }, { populate: ["messages"] });
|
throw new BadRequestError("Invalid session or user ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await ChatSessionModel.findOne({ _id: sendDto.sessionId, user: userId }).lean();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new NotFoundException("Chat session not found");
|
throw new NotFoundError("Chat session not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.status !== ChatSessionStatus.ACTIVE) {
|
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) {
|
if (!user) {
|
||||||
throw new NotFoundException("User not found");
|
throw new NotFoundError("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user message
|
// Create user message
|
||||||
const userMessage = new ChatMessage();
|
const userMessage = await ChatMessageModel.create({
|
||||||
userMessage.content = sendDto.content;
|
content: sendDto.content,
|
||||||
userMessage.type = MessageType.USER;
|
type: MessageType.USER,
|
||||||
userMessage.session = session;
|
session: sendDto.sessionId,
|
||||||
userMessage.sender = user;
|
sender: userId,
|
||||||
userMessage.responseToId = sendDto.responseToId;
|
responseToId: sendDto.responseToId,
|
||||||
userMessage.metadata = sendDto.metadata;
|
metadata: sendDto.metadata,
|
||||||
|
});
|
||||||
await em.persistAndFlush(userMessage);
|
|
||||||
|
|
||||||
// Update session last message time
|
// Update session last message time
|
||||||
await this.chatSessionRepo.updateLastMessageTime(session.id, em);
|
await this.chatSessionRepo.updateLastMessageTime(sendDto.sessionId);
|
||||||
|
|
||||||
// Generate bot response using LangChain
|
// Generate bot response using LangChain
|
||||||
this.generateBotResponseWithLangChain(session, userMessage, userId);
|
this.generateBotResponseWithLangChain(sendDto.sessionId, userMessage._id.toString(), userId);
|
||||||
|
|
||||||
return this.mapMessageToDto(userMessage);
|
return this.mapMessageToDto(userMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
async sendMessageStreamWithLangChain(userId: string, sendDto: SendMessageDto) {
|
async sendMessageStreamWithLangChain(userId: string, sendDto: SendMessageDto) {
|
||||||
const em = this.em.fork();
|
if (!isValidObjectId(sendDto.sessionId) || !isValidObjectId(userId)) {
|
||||||
const session = await em.findOne(ChatSession, { id: sendDto.sessionId, user: userId }, { populate: ["messages"] });
|
throw new BadRequestError("Invalid session or user ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await ChatSessionModel.findOne({ _id: sendDto.sessionId, user: userId }).lean();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new NotFoundException("Chat session not found");
|
throw new NotFoundError("Chat session not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.status !== ChatSessionStatus.ACTIVE) {
|
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) {
|
if (!user) {
|
||||||
throw new NotFoundException("User not found");
|
throw new NotFoundError("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user message
|
// Create user message
|
||||||
const userMessage = new ChatMessage();
|
const userMessage = await ChatMessageModel.create({
|
||||||
userMessage.content = sendDto.content;
|
content: sendDto.content,
|
||||||
userMessage.type = MessageType.USER;
|
type: MessageType.USER,
|
||||||
userMessage.session = session;
|
session: sendDto.sessionId,
|
||||||
userMessage.sender = user;
|
sender: userId,
|
||||||
userMessage.responseToId = sendDto.responseToId;
|
responseToId: sendDto.responseToId,
|
||||||
userMessage.metadata = sendDto.metadata;
|
metadata: sendDto.metadata,
|
||||||
|
});
|
||||||
await em.persistAndFlush(userMessage);
|
|
||||||
|
|
||||||
// Update session last message time
|
// Update session last message time
|
||||||
await this.chatSessionRepo.updateLastMessageTime(session.id, em);
|
await this.chatSessionRepo.updateLastMessageTime(sendDto.sessionId);
|
||||||
|
|
||||||
// Get conversation history for context
|
// 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
|
// Build context for LangChain
|
||||||
const context: IChatContext = {
|
const context: IChatContext = {
|
||||||
userId,
|
userId,
|
||||||
sessionId: session.id,
|
sessionId: sendDto.sessionId,
|
||||||
conversationHistory: history.reverse().map((msg) => ({
|
conversationHistory: history.reverse().map((msg) => ({
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
type: msg.type as "user" | "bot" | "system",
|
type: msg.type as "user" | "bot" | "system",
|
||||||
@@ -424,18 +429,20 @@ export class ChatbotService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
private async generateBotResponseWithLangChain(sessionId: string, userMessageId: string, userId: string): Promise<void> {
|
||||||
|
|
||||||
private async generateBotResponseWithLangChain(session: ChatSession, userMessage: ChatMessage, userId: string): Promise<void> {
|
|
||||||
const em = this.em.fork();
|
|
||||||
try {
|
try {
|
||||||
// Get conversation history
|
// 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
|
// Build context for LangChain
|
||||||
const context: IChatContext = {
|
const context: IChatContext = {
|
||||||
userId,
|
userId,
|
||||||
sessionId: session.id,
|
sessionId,
|
||||||
conversationHistory: history.reverse().map((msg) => ({
|
conversationHistory: history.reverse().map((msg) => ({
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
type: msg.type as "user" | "bot" | "system",
|
type: msg.type as "user" | "bot" | "system",
|
||||||
@@ -445,51 +452,51 @@ export class ChatbotService {
|
|||||||
userPreferences: session.context,
|
userPreferences: session.context,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const userMessage = await ChatMessageModel.findById(userMessageId);
|
||||||
|
if (!userMessage) {
|
||||||
|
throw new NotFoundError("User message not found");
|
||||||
|
}
|
||||||
|
|
||||||
// Generate response using LangChain
|
// Generate response using LangChain
|
||||||
const langChainResponse = await this.langChainService.generateResponse(userMessage.content, context);
|
const langChainResponse = await this.langChainService.generateResponse(userMessage.content, context);
|
||||||
|
|
||||||
// Create bot message
|
// Create bot message
|
||||||
const botMessage = new ChatMessage();
|
const botMessage = await ChatMessageModel.create({
|
||||||
botMessage.content = langChainResponse.message;
|
content: langChainResponse.message,
|
||||||
botMessage.type = MessageType.BOT;
|
type: MessageType.BOT,
|
||||||
botMessage.session = session;
|
session: sessionId,
|
||||||
botMessage.responseToId = userMessage.id;
|
responseToId: userMessageId,
|
||||||
botMessage.tokensUsed = langChainResponse.tokensUsed;
|
tokensUsed: langChainResponse.tokensUsed,
|
||||||
botMessage.metadata = {
|
metadata: {
|
||||||
confidence: langChainResponse.confidence,
|
confidence: langChainResponse.confidence,
|
||||||
sources: langChainResponse.sources,
|
sources: langChainResponse.sources,
|
||||||
llmContext: langChainResponse.context,
|
llmContext: langChainResponse.context,
|
||||||
provider: "langchain",
|
provider: "langchain",
|
||||||
documentsRetrieved: langChainResponse.context?.documentsRetrieved || 0,
|
documentsRetrieved: langChainResponse.context?.documentsRetrieved || 0,
|
||||||
};
|
},
|
||||||
|
});
|
||||||
await em.persistAndFlush(botMessage);
|
|
||||||
|
|
||||||
// Update session last message time
|
// 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) {
|
} 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
|
// Fallback to regular LLM service
|
||||||
this.logger.log(`Falling back to regular LLM service for session ${session.id}`);
|
this.logger.info(`Falling back to regular LLM service for session ${sessionId}`);
|
||||||
await this.generateBotResponse(session, userMessage, userId);
|
await this.generateBotResponse(sessionId, userMessageId, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
async refreshLangChainData() {
|
async refreshLangChainData() {
|
||||||
try {
|
try {
|
||||||
await this.langChainService.refreshVectorStore();
|
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" };
|
return { message: "LangChain vector store refreshed successfully" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error("Failed to refresh LangChain vector store", error);
|
this.logger.error("Failed to refresh LangChain vector store", error);
|
||||||
throw new Error("Failed to refresh training data");
|
throw new Error("Failed to refresh training data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import { EntityManager } from "@mikro-orm/core";
|
import { injectable } from "inversify";
|
||||||
import { Injectable, Logger } from "@nestjs/common";
|
|
||||||
|
|
||||||
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 { 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 {
|
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[] }> {
|
async getRelevantContext(message: string, context: IChatContext): Promise<{ data: string[]; sources: string[] }> {
|
||||||
const relevantData: string[] = [];
|
const relevantData: string[] = [];
|
||||||
@@ -26,15 +23,22 @@ export class DataContextService {
|
|||||||
// Analyze message to determine what data to fetch
|
// Analyze message to determine what data to fetch
|
||||||
const keywords = this.extractKeywords(message);
|
const keywords = this.extractKeywords(message);
|
||||||
|
|
||||||
// Fetch relevant data based on keywords and user context
|
// TODO: Implement actual data fetching based on your project's models
|
||||||
await Promise.all([
|
// For now, this returns empty data - you can extend this to fetch:
|
||||||
this.getBusinessData(keywords, context.userId, relevantData, sources),
|
// - Product data from ProductModel
|
||||||
this.getInvoiceData(keywords, context.userId, relevantData, sources),
|
// - Category data from CategoryModel
|
||||||
this.getPaymentData(keywords, context.userId, relevantData, sources),
|
// - Order data from OrderModel
|
||||||
this.getCompanyData(keywords, context.userId, relevantData, sources),
|
// - User data from UserModel
|
||||||
this.getTicketData(keywords, context.userId, relevantData, sources),
|
// etc.
|
||||||
this.getIndustryData(keywords, relevantData, sources),
|
|
||||||
]);
|
// 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 };
|
return { data: relevantData, sources };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -53,174 +57,19 @@ export class DataContextService {
|
|||||||
.filter((word) => word.length > 2 && !commonWords.includes(word));
|
.filter((word) => word.length > 2 && !commonWords.includes(word));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBusinessData(keywords: string[], userId: string, relevantData: string[], sources: string[]): Promise<void> {
|
// Helper methods for keyword detection - can be extended
|
||||||
if (this.hasBusinessKeywords(keywords)) {
|
private hasProductKeywords(keywords: string[]): boolean {
|
||||||
try {
|
const productTerms = ["product", "item", "goods", "merchandise", "کالا", "محصول"];
|
||||||
const businesses = await this.em.find(Business, { danakSubscriptionId: userId }, { limit: 5, populate: ["users"] });
|
return keywords.some((keyword) => productTerms.includes(keyword));
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getInvoiceData(keywords: string[], userId: string, relevantData: string[], sources: string[]): Promise<void> {
|
private hasOrderKeywords(keywords: string[]): boolean {
|
||||||
if (this.hasInvoiceKeywords(keywords)) {
|
const orderTerms = ["order", "purchase", "buy", "cart", "سفارش", "خرید"];
|
||||||
try {
|
return keywords.some((keyword) => orderTerms.includes(keyword));
|
||||||
// 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 async getPaymentData(keywords: string[], userId: string, relevantData: string[], sources: string[]): Promise<void> {
|
private hasCategoryKeywords(keywords: string[]): boolean {
|
||||||
if (this.hasPaymentKeywords(keywords)) {
|
const categoryTerms = ["category", "type", "kind", "دسته", "دستهبندی"];
|
||||||
try {
|
return keywords.some((keyword) => categoryTerms.includes(keyword));
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,38 +3,38 @@ import { StringOutputParser } from "@langchain/core/output_parsers";
|
|||||||
import { PromptTemplate } from "@langchain/core/prompts";
|
import { PromptTemplate } from "@langchain/core/prompts";
|
||||||
import { RunnableSequence } from "@langchain/core/runnables";
|
import { RunnableSequence } from "@langchain/core/runnables";
|
||||||
import { ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
|
import { ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
|
||||||
import { EntityManager } from "@mikro-orm/core";
|
import { injectable } from "inversify";
|
||||||
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
|
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
|
||||||
import { MemoryVectorStore } from "langchain/vectorstores/memory";
|
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 { CHATBOT_CONSTANTS } from "../constants/chatbot.constants";
|
||||||
import { IChatContext, IChatbotResponse } from "../interfaces/chatbot.interface";
|
import { IChatContext, IChatbotResponse } from "../interfaces/chatbot.interface";
|
||||||
|
import { Logger } from "../../../core/logging/logger";
|
||||||
|
|
||||||
@Injectable()
|
@injectable()
|
||||||
export class LangChainService implements OnModuleInit {
|
export class LangChainService {
|
||||||
private readonly logger = new Logger(LangChainService.name);
|
private readonly logger: Logger;
|
||||||
private embeddings: GoogleGenerativeAIEmbeddings;
|
private embeddings: GoogleGenerativeAIEmbeddings;
|
||||||
private llm: ChatGoogleGenerativeAI;
|
private llm: ChatGoogleGenerativeAI;
|
||||||
private vectorStore: MemoryVectorStore;
|
private vectorStore: MemoryVectorStore;
|
||||||
private textSplitter: RecursiveCharacterTextSplitter;
|
private textSplitter: RecursiveCharacterTextSplitter;
|
||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
|
|
||||||
constructor(
|
constructor() {
|
||||||
private em: EntityManager,
|
this.logger = new Logger("LangChainService");
|
||||||
private configService: ConfigService,
|
const apiKey = process.env.GEMINI_API_KEY;
|
||||||
) {
|
if (!apiKey) {
|
||||||
|
throw new Error("GEMINI_API_KEY is required");
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize Google Gemini components
|
// Initialize Google Gemini components
|
||||||
this.embeddings = new GoogleGenerativeAIEmbeddings({
|
this.embeddings = new GoogleGenerativeAIEmbeddings({
|
||||||
apiKey: this.configService.getOrThrow("GEMINI_API_KEY"),
|
apiKey,
|
||||||
modelName: "embedding-001", // Google's embedding model
|
modelName: "embedding-001", // Google's embedding model
|
||||||
});
|
});
|
||||||
|
|
||||||
this.llm = new ChatGoogleGenerativeAI({
|
this.llm = new ChatGoogleGenerativeAI({
|
||||||
apiKey: this.configService.getOrThrow("GEMINI_API_KEY"),
|
apiKey,
|
||||||
model: CHATBOT_CONSTANTS.DEFAULT_MODEL,
|
model: CHATBOT_CONSTANTS.DEFAULT_MODEL,
|
||||||
temperature: CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE,
|
temperature: CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE,
|
||||||
maxOutputTokens: CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS,
|
maxOutputTokens: CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS,
|
||||||
@@ -45,18 +45,20 @@ export class LangChainService implements OnModuleInit {
|
|||||||
chunkOverlap: 200,
|
chunkOverlap: 200,
|
||||||
separators: ["\n\n", "\n", ".", "!", "?", "؟", "!", ".", " ", ""],
|
separators: ["\n\n", "\n", ".", "!", "?", "؟", "!", ".", " ", ""],
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
// Initialize vector store asynchronously
|
||||||
await this.initializeVectorStore();
|
this.initializeVectorStore().catch((error) => {
|
||||||
|
this.logger.error("Failed to initialize vector store", error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initializeVectorStore() {
|
private async initializeVectorStore() {
|
||||||
try {
|
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
|
// Load documents for training
|
||||||
const documents = await this.loadCompanyAndIndustryDocuments();
|
// TODO: Load actual data from your database models (Product, Category, etc.)
|
||||||
|
const documents = await this.loadDocuments();
|
||||||
|
|
||||||
if (documents.length === 0) {
|
if (documents.length === 0) {
|
||||||
this.logger.warn(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.NO_DATA_FOUND);
|
this.logger.warn(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.NO_DATA_FOUND);
|
||||||
@@ -68,7 +70,7 @@ export class LangChainService implements OnModuleInit {
|
|||||||
// Split documents into chunks
|
// Split documents into chunks
|
||||||
const splitDocs = await this.textSplitter.splitDocuments(documents);
|
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(
|
CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.DOCUMENTS_SPLIT.replace("{totalDocs}", documents.length.toString()).replace(
|
||||||
"{chunks}",
|
"{chunks}",
|
||||||
splitDocs.length.toString(),
|
splitDocs.length.toString(),
|
||||||
@@ -79,7 +81,7 @@ export class LangChainService implements OnModuleInit {
|
|||||||
this.vectorStore = await MemoryVectorStore.fromDocuments(splitDocs, this.embeddings);
|
this.vectorStore = await MemoryVectorStore.fromDocuments(splitDocs, this.embeddings);
|
||||||
|
|
||||||
this.isInitialized = true;
|
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) {
|
} catch (error) {
|
||||||
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.VECTOR_STORE_ERROR, error);
|
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.VECTOR_STORE_ERROR, error);
|
||||||
// Fallback to empty vector store
|
// 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 documents: Document[] = [];
|
||||||
const em = this.em.fork();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load companies data with their products and services
|
// TODO: Load actual data from your database
|
||||||
const companies = await em.find(Company, { isActive: true, deletedAt: null }, { populate: ["industry", "business", "products", "services"] });
|
// Example:
|
||||||
|
// const products = await ProductModel.find({ ... }).limit(100);
|
||||||
companies.forEach((company) => {
|
// products.forEach((product) => {
|
||||||
// Company basic information using template
|
// documents.push(
|
||||||
const companyContent = this.replaceTemplate(CHATBOT_CONSTANTS.COMPANY_DATA_TEMPLATES.COMPANY_INFO, {
|
// new Document({
|
||||||
name: company.name,
|
// pageContent: `Product: ${product.title_fa} - Description: ${product.description}`,
|
||||||
ceo: company.chiefExecutiveOfficer,
|
// metadata: { type: "product", id: product._id.toString() },
|
||||||
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,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add comprehensive Farsi guidance documents from constants
|
// Add comprehensive Farsi guidance documents from constants
|
||||||
const companyGuidanceDocuments = [
|
const companyGuidanceDocuments = [
|
||||||
@@ -252,7 +144,7 @@ export class LangChainService implements OnModuleInit {
|
|||||||
|
|
||||||
documents.push(...companyGuidanceDocuments);
|
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;
|
return documents;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.LOADING_ERROR, error);
|
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.LOADING_ERROR, error);
|
||||||
@@ -363,7 +255,8 @@ export class LangChainService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refreshVectorStore(): Promise<void> {
|
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();
|
await this.initializeVectorStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,17 +265,6 @@ export class LangChainService implements OnModuleInit {
|
|||||||
return Math.ceil(text.length / 4);
|
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 {
|
get initialized(): boolean {
|
||||||
return this.isInitialized;
|
return this.isInitialized;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
import { GoogleGenerativeAI, HarmBlockThreshold, HarmCategory } from "@google/generative-ai";
|
import { GoogleGenerativeAI, HarmBlockThreshold, HarmCategory } from "@google/generative-ai";
|
||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { inject, injectable } from "inversify";
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
|
|
||||||
import { DataContextService } from "./data-context.service";
|
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 { CHATBOT_CONSTANTS } from "../constants/chatbot.constants";
|
||||||
import { IChatContext, IChatbotResponse, ILLMConfig } from "../interfaces/chatbot.interface";
|
import { IChatContext, IChatbotResponse, ILLMConfig } from "../interfaces/chatbot.interface";
|
||||||
|
|
||||||
@Injectable()
|
@injectable()
|
||||||
export class LLMService {
|
export class LLMService {
|
||||||
private readonly logger = new Logger(LLMService.name);
|
private readonly logger: Logger;
|
||||||
private genAI: GoogleGenerativeAI;
|
private genAI: GoogleGenerativeAI;
|
||||||
private config: ILLMConfig;
|
private config: ILLMConfig;
|
||||||
|
|
||||||
constructor(
|
constructor(@inject(IOCTYPES.ChatbotDataContextService) private dataContextService: DataContextService) {
|
||||||
private configService: ConfigService,
|
this.logger = new Logger("LLMService");
|
||||||
private dataContextService: DataContextService,
|
|
||||||
) {
|
|
||||||
this.config = {
|
this.config = {
|
||||||
model: this.configService.get("GEMINI_MODEL", CHATBOT_CONSTANTS.DEFAULT_MODEL),
|
model: process.env.GEMINI_MODEL || CHATBOT_CONSTANTS.DEFAULT_MODEL,
|
||||||
temperature: Number(this.configService.get("GEMINI_TEMPERATURE", CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE)),
|
temperature: Number(process.env.GEMINI_TEMPERATURE || CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE),
|
||||||
maxTokens: Number(this.configService.get("GEMINI_MAX_TOKENS", CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS)),
|
maxTokens: Number(process.env.GEMINI_MAX_TOKENS || CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS),
|
||||||
topP: Number(this.configService.get("GEMINI_TOP_P", 0.95)),
|
topP: Number(process.env.GEMINI_TOP_P || "0.95"),
|
||||||
apiKey: this.configService.getOrThrow("GEMINI_API_KEY"),
|
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);
|
this.genAI = new GoogleGenerativeAI(this.config.apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +165,9 @@ export class LLMService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const history = [];
|
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) {
|
for (const msg of recentHistory) {
|
||||||
history.push({
|
history.push({
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { inject, injectable } from "inversify";
|
||||||
import { JwtService } from "@nestjs/jwt";
|
|
||||||
import { Socket } from "socket.io";
|
import { Socket } from "socket.io";
|
||||||
|
|
||||||
import { WebSocketMessage } from "../../../common/enums/message.enum";
|
import { WebSocketMessage } from "../../../common/enums/message.enum";
|
||||||
import { ITokenPayload } from "../../auth/interfaces/IToken-payload";
|
import { AuthTokenPayload } from "../../../common/types/jwt.type";
|
||||||
import { extractTokenFromClient } from "../../utils/providers/extract-token.utils";
|
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 { WEBSOCKET_EVENTS } from "../constants/chatbot.constants";
|
||||||
import { AuthenticationResult, WebSocketErrorContext } from "../interfaces/websocket.interface";
|
import { AuthenticationResult, WebSocketErrorContext } from "../interfaces/websocket.interface";
|
||||||
|
|
||||||
@@ -12,11 +14,35 @@ import { AuthenticationResult, WebSocketErrorContext } from "../interfaces/webso
|
|||||||
* Service responsible for WebSocket authentication logic
|
* Service responsible for WebSocket authentication logic
|
||||||
* Follows single responsibility principle and provides reusable authentication methods
|
* Follows single responsibility principle and provides reusable authentication methods
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@injectable()
|
||||||
export class WebSocketAuthService {
|
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
|
* Authenticates a WebSocket client and returns the result
|
||||||
@@ -26,7 +52,7 @@ export class WebSocketAuthService {
|
|||||||
*/
|
*/
|
||||||
async authenticateClient(client: Socket): Promise<AuthenticationResult> {
|
async authenticateClient(client: Socket): Promise<AuthenticationResult> {
|
||||||
try {
|
try {
|
||||||
const token = extractTokenFromClient(client);
|
const token = this.extractTokenFromClient(client);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
this.logAuthenticationAttempt(client, false, "No token provided");
|
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");
|
this.logAuthenticationAttempt(client, false, "Invalid token payload");
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -46,14 +72,14 @@ export class WebSocketAuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store user data in client
|
// Store user data in client (convert sub to id for compatibility)
|
||||||
client.data.user = payload;
|
client.data.user = { id: payload.sub, sub: payload.sub };
|
||||||
|
|
||||||
this.logAuthenticationAttempt(client, true, undefined, payload.id);
|
this.logAuthenticationAttempt(client, true, undefined, payload.sub);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
user: payload,
|
user: { id: payload.sub, sub: payload.sub },
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = this.getAuthErrorMessage(error);
|
const errorMessage = this.getAuthErrorMessage(error);
|
||||||
@@ -90,7 +116,7 @@ export class WebSocketAuthService {
|
|||||||
* @param client - Socket.IO client
|
* @param client - Socket.IO client
|
||||||
* @param user - Authenticated user payload
|
* @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, {
|
client.emit(WEBSOCKET_EVENTS.AUTHENTICATED, {
|
||||||
message: WebSocketMessage.AUTHENTICATED,
|
message: WebSocketMessage.AUTHENTICATED,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -151,7 +177,7 @@ export class WebSocketAuthService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.logger.log(`WebSocket authentication successful`, logData);
|
this.logger.info(`WebSocket authentication successful`, logData);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`WebSocket authentication failed`, logData);
|
this.logger.warn(`WebSocket authentication failed`, logData);
|
||||||
}
|
}
|
||||||
@@ -161,7 +187,7 @@ export class WebSocketAuthService {
|
|||||||
* Maps JWT errors to user-friendly messages
|
* Maps JWT errors to user-friendly messages
|
||||||
*/
|
*/
|
||||||
private getAuthErrorMessage(error: any): string {
|
private getAuthErrorMessage(error: any): string {
|
||||||
if (error?.name === "TokenExpiredError") {
|
if (error instanceof jwtExpiredErr || error?.name === "TokenExpiredError") {
|
||||||
return WebSocketMessage.TOKEN_EXPIRED;
|
return WebSocketMessage.TOKEN_EXPIRED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,37 @@
|
|||||||
import { EntityManager, EntityRepository } from "@mikro-orm/postgresql";
|
import { BaseRepository } from "../../../common/base/repository";
|
||||||
|
import { ChatMessageModel } from "../models/chat-message.model";
|
||||||
|
import { IChatMessage, MessageStatus } from "../models/Abstraction/IChatMessage";
|
||||||
|
|
||||||
import { ChatMessage, MessageStatus } from "../entities/chat-message.entity";
|
class ChatMessageRepository extends BaseRepository<IChatMessage> {
|
||||||
|
constructor() {
|
||||||
export class ChatMessageRepository extends EntityRepository<ChatMessage> {
|
super(ChatMessageModel);
|
||||||
async findBySessionWithPagination(sessionId: string, offset = 0, limit = 50, em?: EntityManager) {
|
|
||||||
const repository = em ? em.getRepository(ChatMessage) : this;
|
|
||||||
return repository.find(
|
|
||||||
{ session: sessionId },
|
|
||||||
{
|
|
||||||
populate: ["sender"],
|
|
||||||
orderBy: { createdAt: "ASC" },
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConversationHistory(sessionId: string, limit = 20, em?: EntityManager) {
|
async findBySessionWithPagination(sessionId: string, offset = 0, limit = 50) {
|
||||||
const repository = em ? em.getRepository(ChatMessage) : this;
|
return this.model
|
||||||
return repository.find(
|
.find({ session: sessionId })
|
||||||
{ session: sessionId },
|
.populate("sender")
|
||||||
{
|
.sort({ createdAt: 1 })
|
||||||
orderBy: { createdAt: "DESC" },
|
.skip(offset)
|
||||||
limit,
|
.limit(limit)
|
||||||
},
|
.exec();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async markAsRead(messageIds: string[], em?: EntityManager) {
|
async getConversationHistory(sessionId: string, limit = 20) {
|
||||||
const repository = em ? em.getRepository(ChatMessage) : this;
|
return this.model
|
||||||
return repository.nativeUpdate({ id: { $in: messageIds } }, { status: MessageStatus.READ });
|
.find({ session: sessionId })
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.limit(limit)
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAsRead(messageIds: string[]) {
|
||||||
|
return this.model.updateMany({ _id: { $in: messageIds } }, { status: MessageStatus.READ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createChatMessageRepository(): ChatMessageRepository {
|
||||||
|
return new ChatMessageRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ChatMessageRepository, createChatMessageRepository };
|
||||||
|
|||||||
@@ -1,33 +1,40 @@
|
|||||||
import { EntityManager, EntityRepository } from "@mikro-orm/postgresql";
|
import { BaseRepository } from "../../../common/base/repository";
|
||||||
|
import { ChatSessionModel, ChatSessionStatus } from "../models/chat-session.model";
|
||||||
|
import { IChatSession } from "../models/Abstraction/IChatSession";
|
||||||
|
|
||||||
import { ChatSession, ChatSessionStatus } from "../entities/chat-session.entity";
|
class ChatSessionRepository extends BaseRepository<IChatSession> {
|
||||||
|
constructor() {
|
||||||
export class ChatSessionRepository extends EntityRepository<ChatSession> {
|
super(ChatSessionModel);
|
||||||
async findByUserWithMessages(userId: string, limit = 10, em?: EntityManager) {
|
|
||||||
const repository = em ? em.getRepository(ChatSession) : this;
|
|
||||||
return repository.find(
|
|
||||||
{ user: userId },
|
|
||||||
{
|
|
||||||
populate: ["messages"],
|
|
||||||
orderBy: { lastMessageAt: "DESC" },
|
|
||||||
limit,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findActiveByUser(userId: string, em?: EntityManager) {
|
async findByUserWithMessages(userId: string, limit = 10) {
|
||||||
const repository = em ? em.getRepository(ChatSession) : this;
|
return this.model
|
||||||
return repository.find(
|
.find({ user: userId })
|
||||||
{ user: userId, status: ChatSessionStatus.ACTIVE },
|
.populate("messages")
|
||||||
{
|
.sort({ lastMessageAt: -1 })
|
||||||
populate: ["messages"],
|
.limit(limit)
|
||||||
orderBy: { lastMessageAt: "DESC" },
|
.exec();
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateLastMessageTime(sessionId: string, em?: EntityManager) {
|
async findActiveByUser(userId: string) {
|
||||||
const repository = em ? em.getRepository(ChatSession) : this;
|
return this.model
|
||||||
return repository.nativeUpdate({ id: sessionId }, { lastMessageAt: new Date() });
|
.find({ user: userId, status: ChatSessionStatus.ACTIVE })
|
||||||
|
.populate("messages")
|
||||||
|
.sort({ lastMessageAt: -1 })
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateLastMessageTime(sessionId: string) {
|
||||||
|
return this.model.findByIdAndUpdate(sessionId, { lastMessageAt: new Date() }, { new: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByUserAndId(sessionId: string, userId: string) {
|
||||||
|
return this.model.findOne({ _id: sessionId, user: userId }).populate("messages").populate("messages.sender").exec();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createChatSessionRepository(): ChatSessionRepository {
|
||||||
|
return new ChatSessionRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ChatSessionRepository, createChatSessionRepository };
|
||||||
|
|||||||
Reference in New Issue
Block a user