forked from Chatbot/v3-api
936 lines
29 KiB
TypeScript
936 lines
29 KiB
TypeScript
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
|
import { InjectModel } from '@nestjs/mongoose';
|
|
import { FilterQuery, Model, Types, UpdateQuery } from 'mongoose';
|
|
import { BaseResponseDTO } from 'src/common/dto/base-response.dto';
|
|
import { TimeHelper } from 'src/common/tools/time-helper';
|
|
import { Sender } from 'src/common/types/sender.type';
|
|
import { ReactEnum } from 'src/common/types/react.type';
|
|
import { AdminModel } from 'src/database/model/admin.model';
|
|
import { DictionariesModel } from 'src/database/model/dictionaries.model';
|
|
import { SessionModel } from 'src/database/model/sessions.model';
|
|
import { UserModel } from 'src/database/model/user.model';
|
|
import { ReactsModel } from 'src/database/model/botReact.model';
|
|
import { AiServiceService } from '../../ai-service/ai-service.service';
|
|
import { PoliciesService } from 'src/policies/policies.service';
|
|
import {
|
|
UserAskQuestion,
|
|
UserRateTheExpert,
|
|
UserReactToMessage,
|
|
} from './dto/user.dto';
|
|
|
|
@Injectable()
|
|
export class UserService {
|
|
constructor(
|
|
@InjectModel(UserModel.name) private readonly user: Model<UserModel>,
|
|
@InjectModel(SessionModel.name)
|
|
private readonly session: Model<SessionModel>,
|
|
@InjectModel(AdminModel.name) private readonly admin: Model<AdminModel>,
|
|
@InjectModel(DictionariesModel.name)
|
|
private readonly dictionaries: Model<DictionariesModel>,
|
|
@InjectModel(ReactsModel.name)
|
|
private readonly reacts: Model<ReactsModel>,
|
|
private readonly aiService: AiServiceService,
|
|
private readonly policiesService: PoliciesService,
|
|
) {}
|
|
|
|
private normalizeReactValue(value: string | ReactEnum): ReactEnum {
|
|
if (value === ReactEnum.like || value === ReactEnum.dislike || value === ReactEnum.nothing) {
|
|
return value as ReactEnum;
|
|
}
|
|
if (typeof value === 'string') {
|
|
const v = value.toLowerCase();
|
|
if (v === 'like') return ReactEnum.like;
|
|
if (v === 'dislike') return ReactEnum.dislike;
|
|
if (v === 'nothing') return ReactEnum.nothing;
|
|
}
|
|
return ReactEnum.nothing;
|
|
}
|
|
|
|
async getMyPolicies(user) {
|
|
try {
|
|
const userId =
|
|
typeof user._id === 'string' ? new Types.ObjectId(user._id) : user._id;
|
|
const data = await this.policiesService.getUserPoliciesWithInstallments(
|
|
userId,
|
|
);
|
|
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', data);
|
|
} catch (err) {
|
|
console.log(err);
|
|
throw new BaseResponseDTO(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
'Failed to fetch policies',
|
|
null,
|
|
);
|
|
}
|
|
}
|
|
|
|
async getProfile(user) {
|
|
try {
|
|
const userId =
|
|
typeof user._id === 'string' ? new Types.ObjectId(user._id) : user._id;
|
|
const userDoc = await this.user.findOne({ _id: userId });
|
|
|
|
if (!userDoc)
|
|
throw new HttpException('user_not_found', HttpStatus.NOT_FOUND);
|
|
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', {
|
|
_id: userDoc._id,
|
|
name: userDoc.name || null,
|
|
family: userDoc.family || null,
|
|
birthDate: userDoc.birthDate || null,
|
|
nationalCode: userDoc.nationalCode || null,
|
|
mobile: userDoc.mobile || null,
|
|
email: userDoc.email || null,
|
|
address: userDoc.address || null,
|
|
username: userDoc.username || null,
|
|
});
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (err instanceof HttpException) {
|
|
throw err;
|
|
}
|
|
throw new BaseResponseDTO(err.status, err.response, null);
|
|
}
|
|
}
|
|
|
|
async getUserLastQuestionsHistory(user) {
|
|
try {
|
|
const now = Date.now() / 1000;
|
|
const userId =
|
|
typeof user._id === 'string' ? new Types.ObjectId(user._id) : user._id;
|
|
const sessions = await this.session
|
|
.find({ userId: userId })
|
|
.select('chatTitle _id createdAt createdISO')
|
|
.lean()
|
|
.exec();
|
|
|
|
// Calculate date boundaries
|
|
const nowMs = Date.now();
|
|
const nowDate = new Date(nowMs);
|
|
|
|
// 7 days ago
|
|
const sevenDaysAgo = nowMs - (7 * 24 * 60 * 60 * 1000);
|
|
|
|
// 6 months ago (using actual month calculation)
|
|
const sixMonthsAgoDate = new Date(nowDate);
|
|
sixMonthsAgoDate.setMonth(sixMonthsAgoDate.getMonth() - 6);
|
|
const sixMonthsAgo = sixMonthsAgoDate.getTime();
|
|
|
|
// 12 months ago (using actual month calculation)
|
|
const twelveMonthsAgoDate = new Date(nowDate);
|
|
twelveMonthsAgoDate.setMonth(twelveMonthsAgoDate.getMonth() - 12);
|
|
const twelveMonthsAgo = twelveMonthsAgoDate.getTime();
|
|
|
|
// Initialize categorized arrays
|
|
const recent: any[] = []; // Last 7 days
|
|
const lastSixMonths: any[] = []; // Last 6 months (excluding last 7 days)
|
|
const previousSixMonths: any[] = []; // 6-12 months ago
|
|
|
|
if (sessions && sessions.length > 0) {
|
|
for (let session of sessions) {
|
|
const sessionData = {
|
|
title: session["chatTitle"],
|
|
sessionId: session["_id"],
|
|
time: session["createdAt"][0],
|
|
date: session["createdAt"][1],
|
|
};
|
|
|
|
// Use createdISO for accurate date comparison
|
|
const sessionDate = session["createdISO"]
|
|
? new Date(session["createdISO"]).getTime()
|
|
: null;
|
|
|
|
if (sessionDate) {
|
|
if (sessionDate >= sevenDaysAgo) {
|
|
// Last 7 days (recent)
|
|
recent.push(sessionData);
|
|
} else if (sessionDate >= sixMonthsAgo) {
|
|
// Last 6 months (excluding last 7 days)
|
|
lastSixMonths.push(sessionData);
|
|
} else if (sessionDate >= twelveMonthsAgo) {
|
|
// Previous 6 months (6-12 months ago)
|
|
previousSixMonths.push(sessionData);
|
|
}
|
|
// Sessions older than 12 months are not included
|
|
} else {
|
|
// Fallback: if createdISO is missing, include in recent (shouldn't happen normally)
|
|
recent.push(sessionData);
|
|
}
|
|
}
|
|
}
|
|
|
|
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', {
|
|
recent, // Last 7 days
|
|
lastSixMonths, // Last 6 months (excluding last 7 days)
|
|
previousSixMonths, // 6-12 months ago
|
|
now: TimeHelper.unix2PersianTimeAndDate(now),
|
|
});
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (err instanceof HttpException) {
|
|
throw err;
|
|
}
|
|
throw new BaseResponseDTO(err.status, err.response, null);
|
|
}
|
|
}
|
|
|
|
async userSessionHistory(user, sessionId) {
|
|
try {
|
|
console.log('userSessionHistory called with sessionId:', sessionId);
|
|
if (!sessionId || !Types.ObjectId.isValid(sessionId)) {
|
|
console.error('Invalid sessionId provided to userSessionHistory:', sessionId);
|
|
throw new HttpException('invalid_session_id', HttpStatus.BAD_REQUEST);
|
|
}
|
|
const newSession = await this.session
|
|
.findOne({
|
|
_id: new Types.ObjectId(sessionId),
|
|
// userId: user._id,
|
|
})
|
|
.exec();
|
|
|
|
if (!newSession) {
|
|
throw new HttpException('session_not_found', HttpStatus.NOT_FOUND);
|
|
}
|
|
|
|
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', {
|
|
sessionId: newSession._id,
|
|
chatTitle: newSession.chatTitle,
|
|
connectedToExpert: newSession.connectedToExpert,
|
|
onlineChatClosed: newSession.onlineChatClosed,
|
|
chatClosed: newSession.chatClosed,
|
|
count: `${newSession.messages.filter((message) => message.sender === Sender.Bot).length}/20`,
|
|
time: newSession.createdAt[0],
|
|
date: newSession.createdAt[1],
|
|
messages: newSession.messages,
|
|
});
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (err instanceof HttpException) {
|
|
throw err;
|
|
}
|
|
throw new BaseResponseDTO(err.status, err.response, null);
|
|
}
|
|
}
|
|
|
|
async rateTheExpert(body: UserRateTheExpert, user) {
|
|
try {
|
|
const now = Date.now() / 1000;
|
|
const userId =
|
|
typeof user._id === 'string' ? new Types.ObjectId(user._id) : user._id;
|
|
// Fetch the session from the `sessions` collection
|
|
const sessionIdToFind = new Types.ObjectId(body.sessionId);
|
|
const newSession = await this.session
|
|
.findOne({
|
|
_id: sessionIdToFind,
|
|
userId: userId, // Ensure the session belongs to the user
|
|
})
|
|
.exec();
|
|
|
|
if (!newSession) {
|
|
throw new HttpException('session_not_found', HttpStatus.NOT_FOUND);
|
|
}
|
|
|
|
if (!newSession.connectedToExpert) {
|
|
throw new HttpException('expert_not_connected', HttpStatus.BAD_REQUEST);
|
|
}
|
|
|
|
// Check if the expert field is empty or null
|
|
if (!newSession.expert || newSession.expert.trim() === '') {
|
|
throw new HttpException('expert_not_assigned', HttpStatus.BAD_REQUEST);
|
|
}
|
|
|
|
// Update the expert rate in the `sessions` collection
|
|
await this.session.updateOne(
|
|
{ _id: sessionIdToFind },
|
|
{
|
|
$set: {
|
|
expertRate: body.rate,
|
|
},
|
|
},
|
|
);
|
|
|
|
// Update the admin's rate in the `admin` collection
|
|
await this.admin.updateOne(
|
|
{ username: newSession.expert },
|
|
{
|
|
$push: {
|
|
rate: {
|
|
sessionId: body.sessionId,
|
|
rate: body.rate,
|
|
createdAt: TimeHelper.unix2PersianTimeAndDate(now),
|
|
createdAtDate: Date.now(),
|
|
},
|
|
},
|
|
},
|
|
);
|
|
|
|
// Return success response
|
|
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', null);
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (err instanceof HttpException) {
|
|
throw err;
|
|
}
|
|
throw new BaseResponseDTO(err.status, err.response, null);
|
|
}
|
|
}
|
|
|
|
async getUsersFaq(user) {
|
|
try {
|
|
const dictionaries = await this.dictionaries.find({
|
|
isActive: true,
|
|
aiCollectionName: { $ne: 'agents' }
|
|
});
|
|
|
|
// Combine all questions from all dictionaries into a single array
|
|
const allQuestions = dictionaries.flatMap(
|
|
(dictionary) => dictionary.questions,
|
|
);
|
|
|
|
// Filter out questions where deleted is true
|
|
const filteredQuestions = allQuestions.filter(
|
|
(question) => question.deleted !== true,
|
|
);
|
|
|
|
// Extract only the `question` field from each question object
|
|
const questionsOnly = filteredQuestions.map(
|
|
(question) => question.question,
|
|
);
|
|
|
|
// Function to shuffle an array using Fisher-Yates algorithm
|
|
const shuffleArray = (array) => {
|
|
for (let i = array.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[array[i], array[j]] = [array[j], array[i]];
|
|
}
|
|
return array;
|
|
};
|
|
|
|
// Shuffle the questions and pick the first 3
|
|
const shuffledQuestions = shuffleArray(questionsOnly);
|
|
const randomQuestions = shuffledQuestions.slice(0, 3);
|
|
|
|
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', randomQuestions);
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (err instanceof HttpException) {
|
|
throw err;
|
|
}
|
|
throw new BaseResponseDTO(err.status, err.response, null);
|
|
}
|
|
}
|
|
|
|
async react(body: UserReactToMessage, userData) {
|
|
try {
|
|
const { sessionId, messageId } = body;
|
|
const normalizedReact = this.normalizeReactValue((body as any).react);
|
|
const sessionObjectId = new Types.ObjectId(sessionId);
|
|
const userObjectId = new Types.ObjectId(userData._id);
|
|
const storedMessageId = this.toStoredMessageId(messageId);
|
|
|
|
// Update the message's react field in the session
|
|
// Use updateOne first to ensure the update happens, then fetch the result
|
|
const updateResult = await this.session.updateOne(
|
|
{
|
|
_id: sessionObjectId,
|
|
'messages.messageId': storedMessageId,
|
|
},
|
|
{
|
|
$set: {
|
|
'messages.$.react': normalizedReact,
|
|
},
|
|
},
|
|
);
|
|
|
|
if (updateResult.matchedCount === 0) {
|
|
throw new HttpException('message_not_found', HttpStatus.NOT_FOUND);
|
|
}
|
|
|
|
// Fetch the updated session to get the reacted message
|
|
const result = await this.session.findById(sessionObjectId);
|
|
if (!result) {
|
|
throw new HttpException('session_not_found', HttpStatus.NOT_FOUND);
|
|
}
|
|
|
|
const reactedMessage = result.messages.find(
|
|
(message) => String(message.messageId) === String(messageId),
|
|
);
|
|
|
|
// Verify the update actually happened
|
|
if (reactedMessage && reactedMessage.react !== normalizedReact) {
|
|
console.warn(`[react] Warning: React update may have failed. Expected: ${normalizedReact}, Got: ${reactedMessage.react}`);
|
|
}
|
|
|
|
if (reactedMessage) {
|
|
const botMessageIndex = result.messages.findIndex(
|
|
(message) => String(message.messageId) === String(messageId),
|
|
);
|
|
|
|
let questionText = null;
|
|
if (botMessageIndex > 0) {
|
|
for (let i = botMessageIndex - 1; i >= 0; i--) {
|
|
if (result.messages[i].sender === Sender.User) {
|
|
questionText = result.messages[i].text;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update or create the react entry in the reacts collection
|
|
// This ensures we only have one react per user per message (the latest one)
|
|
await this.reacts.findOneAndUpdate(
|
|
{
|
|
sessionId: sessionObjectId,
|
|
messageId: storedMessageId,
|
|
userId: userObjectId,
|
|
},
|
|
{
|
|
$set: {
|
|
question: questionText,
|
|
answer: reactedMessage.text,
|
|
sender: reactedMessage.sender,
|
|
react: normalizedReact,
|
|
createdAt: TimeHelper.unix2PersianTimeAndDate(Date.now() / 1000),
|
|
createdISO: new Date(),
|
|
},
|
|
},
|
|
{
|
|
upsert: true, // Create if doesn't exist, update if it does
|
|
new: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
|
|
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', {
|
|
sessionId: result._id,
|
|
chatTitle: result.chatTitle,
|
|
count: `${result.messages.filter((message) => message.sender === Sender.Bot).length}/20`,
|
|
time: result.createdAt[0],
|
|
date: result.createdAt[1],
|
|
messages: result.messages,
|
|
});
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (err instanceof HttpException) {
|
|
throw err;
|
|
}
|
|
throw new BaseResponseDTO(err.status, err.response, null);
|
|
}
|
|
}
|
|
|
|
private toStoredMessageId(messageId: string): Types.ObjectId | string {
|
|
if (
|
|
Types.ObjectId.isValid(messageId) &&
|
|
String(new Types.ObjectId(messageId)) === String(messageId)
|
|
) {
|
|
return new Types.ObjectId(messageId);
|
|
}
|
|
return messageId;
|
|
}
|
|
|
|
public async insertNewChatNewStructure(sessionId, text, sender, now) {
|
|
const newMessage = {
|
|
messageId: new Types.ObjectId(),
|
|
text: text,
|
|
sender: sender,
|
|
react: sender === Sender.User ? 'Nothing' : null,
|
|
createdAt: TimeHelper.unix2PersianTimeAndDate(now),
|
|
createdISO: Date.now(),
|
|
};
|
|
|
|
await this.session.updateOne(
|
|
{ _id: sessionId },
|
|
{
|
|
$push: {
|
|
messages: newMessage,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
public async editChatMessage(
|
|
sessionId: string,
|
|
messageId: string,
|
|
newMessageText: string,
|
|
): Promise<any> {
|
|
const sessionObjectId = new Types.ObjectId(sessionId);
|
|
const storedMessageId = this.toStoredMessageId(messageId);
|
|
|
|
const result = await this.session.findOneAndUpdate(
|
|
{
|
|
_id: sessionObjectId,
|
|
'messages.messageId': storedMessageId,
|
|
},
|
|
{
|
|
$set: {
|
|
'messages.$.text': newMessageText,
|
|
'messages.$.edited': true,
|
|
},
|
|
},
|
|
{ new: true },
|
|
);
|
|
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
|
|
// Find the updated message within the session's messages array
|
|
const updatedMessage = result.messages.find(
|
|
(msg) => msg.messageId.toString() === messageId,
|
|
);
|
|
return {
|
|
...updatedMessage,
|
|
sender: updatedMessage.sender, // Ensure sender role is explicitly returned
|
|
}; }
|
|
|
|
async ask(body: UserAskQuestion, user) {
|
|
try {
|
|
if (!body.question) {
|
|
throw new HttpException('question_required', HttpStatus.BAD_REQUEST);
|
|
}
|
|
|
|
const now = Date.now() / 1000;
|
|
|
|
if (!body.sessionId) {
|
|
return await this.handleNewSession(body, user, now);
|
|
} else {
|
|
return await this.handleExistingSession(body, user, now);
|
|
}
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (err instanceof HttpException) {
|
|
throw err;
|
|
}
|
|
throw new BaseResponseDTO(err.status, err.response, null);
|
|
}
|
|
}
|
|
|
|
async handleNewSession(body, user, now) {
|
|
try {
|
|
console.log('new session');
|
|
let aiResponse;
|
|
let isAiServiceAvailable = true;
|
|
|
|
try {
|
|
const timeoutMs = parseInt(process.env.AI_SERVICE_TIMEOUT || '65000', 10); // Default 60 seconds
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
setTimeout(() => {
|
|
reject(
|
|
new HttpException('ai_service_timeout', HttpStatus.GATEWAY_TIMEOUT),
|
|
);
|
|
}, timeoutMs);
|
|
});
|
|
|
|
const aiRequest = await this.buildAiRequestPayload(body, user, []);
|
|
|
|
aiResponse = await Promise.race([
|
|
this.aiService.ask(aiRequest, null),
|
|
timeoutPromise,
|
|
]);
|
|
|
|
if (aiResponse.statusCode == 500) {
|
|
isAiServiceAvailable = false;
|
|
}
|
|
} catch (aiError) {
|
|
// Catch any AI service errors (timeout, network errors, 500 errors, etc.)
|
|
console.log('AI service error caught:', aiError);
|
|
isAiServiceAvailable = false;
|
|
}
|
|
|
|
let messages;
|
|
if (!isAiServiceAvailable) {
|
|
// AI service is unavailable, insert fallback message
|
|
messages = [
|
|
{
|
|
messageId: new Types.ObjectId(),
|
|
text: body.question,
|
|
sender: Sender.User,
|
|
react: 'Nothing',
|
|
createdAt: TimeHelper.unix2PersianTimeAndDate(now),
|
|
createdISO: Date.now(),
|
|
},
|
|
{
|
|
messageId: new Types.ObjectId(),
|
|
text: "به دلیل اختلال در سیستم زیر ساخت های کشور سرویس هوش مصنوعی در دسترس نیست. لطفا جهت دریافت راهنمایی به کارشناس متصل شوید. ",
|
|
sender: Sender.Bot,
|
|
react: null,
|
|
createdAt: TimeHelper.unix2PersianTimeAndDate(now),
|
|
createdISO: Date.now(),
|
|
},
|
|
];
|
|
} else {
|
|
// AI service responded successfully
|
|
messages = [
|
|
{
|
|
messageId: new Types.ObjectId(),
|
|
text: body.question,
|
|
sender: Sender.User,
|
|
react: 'Nothing',
|
|
createdAt: TimeHelper.unix2PersianTimeAndDate(now),
|
|
createdISO: Date.now(),
|
|
},
|
|
{
|
|
messageId: new Types.ObjectId(),
|
|
text: aiResponse,
|
|
sender: Sender.Bot,
|
|
react: null,
|
|
createdAt: TimeHelper.unix2PersianTimeAndDate(now),
|
|
createdISO: Date.now(),
|
|
},
|
|
];
|
|
}
|
|
|
|
const userId =
|
|
typeof user._id === 'string' ? new Types.ObjectId(user._id) : user._id;
|
|
|
|
const newSession = {
|
|
_id: new Types.ObjectId(),
|
|
userId: userId,
|
|
chatTitle: body.question, // TODO: AI wrap-up needed
|
|
connectedToExpert: false,
|
|
onlineChatClosed: false,
|
|
chatClosed: false,
|
|
expert: '',
|
|
expertRate: null,
|
|
createdAt: TimeHelper.unix2PersianTimeAndDate(now),
|
|
createdISO: Date.now(),
|
|
messages: messages,
|
|
};
|
|
|
|
await this.session.create(newSession);
|
|
|
|
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', {
|
|
sessionId: newSession._id,
|
|
count: `1/20`,
|
|
date: TimeHelper.unix2PersianTimeAndDate(now)[1],
|
|
time: TimeHelper.unix2PersianTimeAndDate(now)[0],
|
|
question: body.question,
|
|
answer: messages[1].text,
|
|
history: newSession.messages,
|
|
});
|
|
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (err instanceof HttpException) {
|
|
throw err;
|
|
}
|
|
throw new BaseResponseDTO(err.status, err.response, null);
|
|
}
|
|
}
|
|
|
|
async handleExistingSession(body, user, now) {
|
|
try {
|
|
const sessionIdToFind = new Types.ObjectId(body.sessionId);
|
|
const userId =
|
|
typeof user._id === 'string' ? new Types.ObjectId(user._id) : user._id;
|
|
const newSession = await this.session
|
|
.findOne({
|
|
_id: sessionIdToFind,
|
|
userId: userId,
|
|
})
|
|
.exec();
|
|
|
|
if (!newSession) {
|
|
throw new HttpException('session_not_found', HttpStatus.NOT_FOUND);
|
|
}
|
|
|
|
if (newSession.chatClosed) {
|
|
throw new HttpException(
|
|
'session_limit_reached',
|
|
HttpStatus.BAD_REQUEST,
|
|
);
|
|
}
|
|
|
|
let aiResponse;
|
|
let isAiServiceAvailable = true;
|
|
const fallbackMessage = "به دلیل اختلال در سیستم زیر ساخت های کشور سرویس هوش مصنوعی در دسترس نیست. لطفا جهت دریافت راهنمایی به کارشناس متصل شوید. ";
|
|
|
|
try {
|
|
// Take last 6 messages (3 complete user-assistant pairs) instead of 7
|
|
// to ensure we have complete pairs for chat_history_raw
|
|
const historyToSend = newSession.messages.slice(-6); // Get the last 6 messages (3 pairs)
|
|
|
|
const transformedData = await this.buildAiRequestPayload(
|
|
body,
|
|
user,
|
|
this.transformChatHistory(historyToSend),
|
|
);
|
|
|
|
aiResponse = await this.aiService.ask(
|
|
transformedData,
|
|
body.sessionId,
|
|
);
|
|
|
|
if (aiResponse.statusCode == 500) {
|
|
isAiServiceAvailable = false;
|
|
}
|
|
} catch (aiError) {
|
|
// Catch any AI service errors (network errors, 500 errors, etc.)
|
|
console.log('AI service error caught in existing session:', aiError);
|
|
isAiServiceAvailable = false;
|
|
}
|
|
|
|
// Insert user question
|
|
await this.insertNewChatNewStructure(
|
|
sessionIdToFind,
|
|
body.question,
|
|
Sender.User,
|
|
now,
|
|
);
|
|
|
|
// Insert AI response or fallback message
|
|
const responseText = isAiServiceAvailable ? aiResponse : fallbackMessage;
|
|
await this.insertNewChatNewStructure(
|
|
sessionIdToFind,
|
|
responseText,
|
|
Sender.Bot,
|
|
now,
|
|
);
|
|
|
|
const updatedSession = await this.session
|
|
.findOne({
|
|
_id: sessionIdToFind,
|
|
userId: userId,
|
|
})
|
|
.exec();
|
|
|
|
const botMessageCount = updatedSession.messages.filter(
|
|
(message) => message.sender === Sender.Bot,
|
|
).length;
|
|
|
|
if (botMessageCount >= 20) {
|
|
await this.session.updateOne(
|
|
{ _id: sessionIdToFind },
|
|
{ $set: { chatClosed: true } },
|
|
);
|
|
}
|
|
|
|
return new BaseResponseDTO(HttpStatus.OK, 'SUCCESS', {
|
|
sessionId: updatedSession._id,
|
|
count: `${botMessageCount}/20`,
|
|
createdAt: TimeHelper.unix2PersianTimeAndDate(now),
|
|
question: body.question,
|
|
answer: responseText,
|
|
history: updatedSession.messages,
|
|
});
|
|
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (err instanceof HttpException) {
|
|
throw err;
|
|
}
|
|
throw new BaseResponseDTO(err.status, err.response, null);
|
|
}
|
|
}
|
|
|
|
transformChatHistory(messages) {
|
|
const history = [];
|
|
for (let i = 0; i < messages.length; i += 2) {
|
|
const userMessage = messages[i];
|
|
const botMessage = messages[i + 1];
|
|
|
|
if (userMessage && botMessage) {
|
|
history.push({
|
|
user: userMessage.text,
|
|
assistant: botMessage.text,
|
|
// userCreatedAt: userMessage.createdAt,
|
|
// botCreatedAt: botMessage.createdAt,
|
|
});
|
|
}
|
|
}
|
|
return history;
|
|
}
|
|
|
|
private async buildAiRequestPayload(
|
|
body: UserAskQuestion,
|
|
user: { _id: Types.ObjectId | string },
|
|
chatHistory: { user: string; assistant: string }[],
|
|
) {
|
|
const userId =
|
|
typeof user._id === 'string' ? new Types.ObjectId(user._id) : user._id;
|
|
const userDoc = await this.user
|
|
.findById(userId)
|
|
.select('nationalCode')
|
|
.lean()
|
|
.exec();
|
|
|
|
const { user_insurance_data, user_installments_data } =
|
|
await this.policiesService.getUserDataForAi(
|
|
userId,
|
|
userDoc?.nationalCode,
|
|
);
|
|
|
|
return {
|
|
user_input: body.question,
|
|
chat_history_raw: chatHistory,
|
|
user_insurance_data,
|
|
user_installments_data,
|
|
};
|
|
}
|
|
|
|
async closeChatSession(userId, sessionId) {
|
|
// ? migrated
|
|
try {
|
|
// Close the chat session in the old structure ** old-struc **
|
|
await this.user.updateOne(
|
|
{ _id: userId },
|
|
{
|
|
$set: {
|
|
'chat.$[chatElement].chatClosed': true,
|
|
},
|
|
},
|
|
{
|
|
arrayFilters: [
|
|
{ 'chatElement.sessionId': new Types.ObjectId(sessionId) },
|
|
],
|
|
},
|
|
);
|
|
|
|
// ** old-struc **
|
|
|
|
// Close the chat session in the new structure ** new-struc **
|
|
const sessionIdToFind = new Types.ObjectId(sessionId);
|
|
await this.session.updateOne(
|
|
{ _id: sessionIdToFind, userId: userId },
|
|
{ $set: { chatClosed: true } },
|
|
);
|
|
|
|
// Log the new structure data for verification ** new-struc **
|
|
await this.session
|
|
.findOne({
|
|
_id: sessionIdToFind,
|
|
userId: userId,
|
|
})
|
|
.exec();
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (err instanceof HttpException) {
|
|
throw err;
|
|
}
|
|
throw new BaseResponseDTO(err.status, err.response, null);
|
|
}
|
|
}
|
|
|
|
async findOneUser(filter: FilterQuery<UserModel>): Promise<UserModel> {
|
|
return await this.user.findOne(filter);
|
|
}
|
|
|
|
async updateOneUser(
|
|
filter: FilterQuery<UserModel>,
|
|
update: UpdateQuery<UserModel>,
|
|
): Promise<any> {
|
|
return await this.user.updateOne(filter, update).lean();
|
|
}
|
|
|
|
// public async toggleExpertActions(
|
|
// userId: string,
|
|
// sessionId: string,
|
|
// field: string,
|
|
// ): Promise<void> {
|
|
// try {
|
|
// // Validate the field
|
|
// if (!['connectedToExpert', 'onlineChatClosed'].includes(field)) {
|
|
// throw new Error('Invalid field specified');
|
|
// }
|
|
|
|
// // Toggle the field in the new structure
|
|
// const sessionIdToFind = new Types.ObjectId(sessionId);
|
|
// const result = await this.session.updateOne(
|
|
// { _id: sessionIdToFind, userId: userId },
|
|
// { $set: { [field]: true } },
|
|
// );
|
|
|
|
// // Check if the update was successful
|
|
// if (result.matchedCount === 0) {
|
|
// throw new HttpException('session_not_found', HttpStatus.NOT_FOUND);
|
|
// }
|
|
|
|
// // Log the new structure data for verification
|
|
// const updatedSession = await this.session
|
|
// .findOne({
|
|
// _id: sessionIdToFind,
|
|
// userId: userId,
|
|
// })
|
|
// .exec();
|
|
// } catch (err) {
|
|
// console.log(err);
|
|
// throw new BaseResponseDTO(err.status, err.response, null);
|
|
// }
|
|
// }
|
|
public async toggleExpertActions(
|
|
userId: string,
|
|
sessionId: string,
|
|
field: string,
|
|
expertId?: string,
|
|
): Promise<void> {
|
|
try {
|
|
console.log('toggleExpertActions called with:', { userId, sessionId, field, expertId });
|
|
if (!['connectedToExpert', 'onlineChatClosed'].includes(field)) {
|
|
throw new Error('Invalid field specified');
|
|
}
|
|
|
|
// Additional validation for expertId when field is connectedToExpert
|
|
if (field === 'connectedToExpert' && !expertId) {
|
|
throw new Error('expertId is required when field is connectedToExpert');
|
|
}
|
|
|
|
const sessionIdToFind = new Types.ObjectId(sessionId);
|
|
const userID =
|
|
typeof userId === 'string' ? new Types.ObjectId(userId) : userId;
|
|
|
|
const session = await this.session.findOne({ _id: sessionIdToFind, userId: userID }).exec();
|
|
|
|
if (!session) {
|
|
throw new HttpException('session_not_found', HttpStatus.NOT_FOUND);
|
|
}
|
|
|
|
// New validation logic
|
|
if (field === 'connectedToExpert' && session.connectedToExpert && session.chatClosed) {
|
|
console.error('User already connected to expert in a closed session for sessionId:', sessionId);
|
|
throw new HttpException('you_have_connected_to_expert_once', HttpStatus.BAD_REQUEST);
|
|
}
|
|
|
|
let updateData: any = { [field]: true };
|
|
|
|
// If connecting to expert, get expert username and add to update
|
|
if (field === 'connectedToExpert' && expertId) {
|
|
console.log('Attempting to find expert with expertId:', expertId);
|
|
const expert = await this.admin
|
|
.findOne({ _id: new Types.ObjectId(expertId) }, { username: 1 })
|
|
.exec();
|
|
|
|
if (!expert) {
|
|
console.error('Expert not found for expertId:', expertId);
|
|
throw new HttpException('expert_not_found', HttpStatus.NOT_FOUND);
|
|
}
|
|
|
|
updateData.expert = expert.username;
|
|
}
|
|
|
|
// Update the session
|
|
console.log('Updating session with sessionIdToFind:', sessionIdToFind, 'and userID:', userID);
|
|
const result = await this.session.updateOne(
|
|
{ _id: sessionIdToFind, userId: userID },
|
|
{ $set: updateData },
|
|
);
|
|
|
|
if (result.matchedCount === 0) {
|
|
throw new HttpException('session_not_found', HttpStatus.NOT_FOUND);
|
|
}
|
|
|
|
await this.session
|
|
.findOne({
|
|
_id: sessionIdToFind,
|
|
userId: userID,
|
|
})
|
|
.exec();
|
|
} catch (err) {
|
|
console.log(err);
|
|
if (err instanceof HttpException) {
|
|
throw err;
|
|
}
|
|
throw new BaseResponseDTO(err.status, err.response, null);
|
|
}
|
|
}
|
|
}
|