292 lines
10 KiB
TypeScript
292 lines
10 KiB
TypeScript
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
|
import { firstValueFrom } from "rxjs";
|
|
|
|
import { AiService } from "./ai.service";
|
|
import { AiMessage } from "../../../common/enums/message.enum";
|
|
import { MessageDetails } from "../../mail-server/interfaces/messages-response.interface";
|
|
import { WildDuckMessagesService } from "../../mail-server/services/wildduck-messages.service";
|
|
import { S3Service } from "../../uploader/providers/s3.service";
|
|
import { GenerateTemplateDto, TemplatePurposeEnum, TemplateStyleEnum, TemplateThemeEnum } from "../DTO/generate-template.dto";
|
|
import { SummarizeEmailDto } from "../DTO/summarize-email.dto";
|
|
import { AudioFormatEnum, SpeedEnum, TextToSpeechDto, VoiceEnum } from "../DTO/text-to-speech.dto";
|
|
import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto";
|
|
import { SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
|
import { AITextToSpeechResponse } from "../interfaces/ai-service.interface";
|
|
import {
|
|
IEmailData,
|
|
ITemplateGenerationRequest,
|
|
SummarizationOptions,
|
|
TemplateGenerationOptions,
|
|
TextToSpeechOptions,
|
|
} from "../interfaces/ai-service.interface";
|
|
|
|
@Injectable()
|
|
export class AiAssistantService {
|
|
constructor(
|
|
private readonly messagesService: WildDuckMessagesService,
|
|
private readonly aiService: AiService,
|
|
private readonly s3Service: S3Service,
|
|
) {}
|
|
|
|
//************************************************* */
|
|
async summarizeEmail(summarizeDto: SummarizeEmailDto, userId: string) {
|
|
const message = await firstValueFrom(this.messagesService.getMessage(userId, summarizeDto.mailbox, summarizeDto.messageId));
|
|
|
|
if (!message) throw new NotFoundException(AiMessage.MESSAGE_NOT_FOUND_OR_ACCESS_DENIED);
|
|
|
|
const emailData = this.extractEmailData(message);
|
|
|
|
const summary = await this.generateEmailSummary(emailData, summarizeDto.length, summarizeDto.tone);
|
|
|
|
return summary;
|
|
}
|
|
|
|
//************************************************* */
|
|
async assistEmailWriting(writeAssistanceDto: WriteEmailAssistanceDto, userId: string) {
|
|
const assistance = await this.generateEmailWritingAssistance(writeAssistanceDto, userId);
|
|
|
|
return { assistance };
|
|
}
|
|
|
|
//************************************************* */
|
|
async generateTemplate(generateTemplateDto: GenerateTemplateDto) {
|
|
if (!generateTemplateDto.description?.trim()) {
|
|
throw new BadRequestException(AiMessage.DESCRIPTION_IS_REQUIRED);
|
|
}
|
|
|
|
const templateRequest: ITemplateGenerationRequest = {
|
|
description: generateTemplateDto.description.trim(),
|
|
options: this.buildTemplateGenerationOptions(generateTemplateDto),
|
|
};
|
|
|
|
const aiResponse = await this.aiService.generateTemplate(templateRequest);
|
|
|
|
return {
|
|
template: {
|
|
name: aiResponse.templateName,
|
|
description: aiResponse.description,
|
|
structure: aiResponse.structure,
|
|
rawHtml: aiResponse.rawHtml,
|
|
colorScheme: aiResponse.colorScheme,
|
|
},
|
|
designInsights: {
|
|
designPrinciples: aiResponse.designPrinciples,
|
|
suggestedImprovements: aiResponse.suggestedImprovements,
|
|
},
|
|
metadata: {
|
|
theme: generateTemplateDto.theme || TemplateThemeEnum.MODERN,
|
|
style: generateTemplateDto.style || TemplateStyleEnum.PROFESSIONAL,
|
|
purpose: generateTemplateDto.purpose || TemplatePurposeEnum.GENERAL,
|
|
generatedAt: new Date().toISOString(),
|
|
},
|
|
};
|
|
}
|
|
|
|
//************************************************* */
|
|
async textToSpeechEmailSummary(textToSpeechDto: TextToSpeechDto, userId: string) {
|
|
const message = await firstValueFrom(this.messagesService.getMessage(userId, textToSpeechDto.mailbox, textToSpeechDto.messageId));
|
|
|
|
if (!message) throw new NotFoundException(AiMessage.MESSAGE_NOT_FOUND_OR_ACCESS_DENIED);
|
|
|
|
const emailData = this.extractEmailData(message);
|
|
const summaryResponse = await this.generateEmailSummary(emailData, SummaryLengthEnum.MEDIUM, SummaryToneEnum.PROFESSIONAL);
|
|
|
|
// const textForSpeech = this.buildTextForSpeech(summaryResponse, emailData);
|
|
const textForSpeech = summaryResponse.summary;
|
|
|
|
const ttsOptions: TextToSpeechOptions = {
|
|
voice: textToSpeechDto.voice || VoiceEnum.ALLOY,
|
|
speed: textToSpeechDto.speed || SpeedEnum.NORMAL,
|
|
format: textToSpeechDto.format || AudioFormatEnum.MP3,
|
|
};
|
|
|
|
const audioResponse = await this.aiService.textToSpeech(textForSpeech, ttsOptions);
|
|
|
|
const fileResponse = await this.saveAudioFile(audioResponse, textToSpeechDto, emailData);
|
|
|
|
return {
|
|
file: fileResponse,
|
|
summary: summaryResponse,
|
|
textUsed: textForSpeech,
|
|
};
|
|
}
|
|
|
|
//************************************************* */
|
|
private buildTemplateGenerationOptions(dto: GenerateTemplateDto): TemplateGenerationOptions {
|
|
return {
|
|
theme: dto.theme,
|
|
style: dto.style,
|
|
purpose: dto.purpose,
|
|
templateName: dto.templateName,
|
|
preferredColors: dto.preferredColors,
|
|
};
|
|
}
|
|
|
|
//************************************************* */
|
|
private extractEmailData(message: MessageDetails) {
|
|
return {
|
|
subject: message.subject,
|
|
content: this.extractTextContent(message),
|
|
htmlContent: this.extractHtmlContent(message),
|
|
from: this.extractSenderInfo(message),
|
|
to: this.extractRecipientInfo(message),
|
|
date: message.date || new Date().toISOString(),
|
|
attachments: message.attachments || [],
|
|
};
|
|
}
|
|
|
|
//************************************************* */
|
|
private extractTextContent(message: MessageDetails): string {
|
|
if (message.text) {
|
|
return typeof message.text === "string" ? message.text : message.text || "";
|
|
}
|
|
|
|
if (message.html) {
|
|
return this.stripHtmlTags(typeof message.html === "string" ? message.html : message.html[0] || "");
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
//************************************************* */
|
|
private extractHtmlContent(message: MessageDetails): string {
|
|
if (message.html) {
|
|
return typeof message.html === "string" ? message.html : message.html[0] || "";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
//************************************************* */
|
|
private extractSenderInfo(message: MessageDetails): string {
|
|
if (message.from && message.from.address) {
|
|
return message.from.name ? `${message.from.name} <${message.from.address}>` : message.from.address;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
//************************************************* */
|
|
private extractRecipientInfo(message: MessageDetails): string[] {
|
|
const recipients: string[] = [];
|
|
|
|
if (message.to && Array.isArray(message.to)) {
|
|
recipients.push(...message.to.map((recipient) => (recipient.name ? `${recipient.name} <${recipient.address}>` : recipient.address)));
|
|
}
|
|
|
|
return recipients;
|
|
}
|
|
|
|
//************************************************* */
|
|
private stripHtmlTags(html: string): string {
|
|
return html
|
|
.replace(/<[^>]*>/g, "")
|
|
.replace(/ /g, " ")
|
|
.trim();
|
|
}
|
|
|
|
//************************************************* */
|
|
private async generateEmailSummary(emailData: IEmailData, length?: SummaryLengthEnum, tone?: SummaryToneEnum) {
|
|
const content = emailData.content || emailData.htmlContent;
|
|
|
|
if (!content || !content.trim()) throw new BadRequestException(AiMessage.EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED);
|
|
|
|
const aiEmailData: IEmailData = {
|
|
subject: emailData.subject,
|
|
content: emailData.content,
|
|
htmlContent: emailData.htmlContent,
|
|
from: emailData.from,
|
|
to: emailData.to,
|
|
date: emailData.date,
|
|
attachments: emailData.attachments,
|
|
};
|
|
|
|
const summarizationOptions: SummarizationOptions = {
|
|
length: length || SummaryLengthEnum.MEDIUM,
|
|
tone: tone || SummaryToneEnum.PROFESSIONAL,
|
|
};
|
|
|
|
const aiResponse = await this.aiService.summarizeEmail(aiEmailData, summarizationOptions);
|
|
|
|
return {
|
|
summary: aiResponse.summary,
|
|
keyPoints: aiResponse.keyPoints,
|
|
suggestedActions: aiResponse.suggestedActions,
|
|
priority: aiResponse.priority,
|
|
sentiment: aiResponse.sentiment,
|
|
};
|
|
}
|
|
|
|
//************************************************* */
|
|
private async saveAudioFile(audioResponse: AITextToSpeechResponse, textToSpeechDto: TextToSpeechDto, emailData: IEmailData) {
|
|
const timestamp = Date.now();
|
|
const sanitizedSubject = emailData.subject?.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 50) || "email_summary";
|
|
const fileName = `tts_${sanitizedSubject}_${timestamp}.${audioResponse.format}`;
|
|
|
|
const s3Key = this.s3Service.generateFileKey(fileName, "audio");
|
|
|
|
const s3Result = await this.s3Service.uploadFile(audioResponse.audioBuffer, s3Key, audioResponse.contentType, {
|
|
originalName: fileName,
|
|
messageId: textToSpeechDto.messageId.toString(),
|
|
mailbox: textToSpeechDto.mailbox,
|
|
voice: textToSpeechDto.voice || VoiceEnum.ALLOY,
|
|
speed: (textToSpeechDto.speed || SpeedEnum.NORMAL).toString(),
|
|
format: audioResponse.format,
|
|
generatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
return {
|
|
fileUrl: s3Result.url,
|
|
fileName,
|
|
format: audioResponse.format,
|
|
contentType: audioResponse.contentType,
|
|
fileSize: audioResponse.audioBuffer.length,
|
|
s3Key: s3Result.key,
|
|
};
|
|
}
|
|
|
|
//************************************************* */
|
|
// private buildTextForSpeech(summaryResponse: AISummaryResponse, emailData: IEmailData): string {
|
|
// let text = `Email Summary: `;
|
|
|
|
// if (emailData.subject) {
|
|
// text += `Subject: ${emailData.subject}. `;
|
|
// }
|
|
|
|
// if (emailData.from) {
|
|
// text += `From: ${emailData.from}. `;
|
|
// }
|
|
|
|
// if (summaryResponse.summary) {
|
|
// text += `Summary: ${summaryResponse.summary}. `;
|
|
// }
|
|
|
|
// if (summaryResponse.keyPoints && summaryResponse.keyPoints.length > 0) {
|
|
// text += `Key Points: ${summaryResponse.keyPoints.join(". ")}. `;
|
|
// }
|
|
|
|
// if (summaryResponse.suggestedActions && summaryResponse.suggestedActions.length > 0) {
|
|
// text += `Suggested Actions: ${summaryResponse.suggestedActions.join(". ")}. `;
|
|
// }
|
|
|
|
// if (summaryResponse.priority) {
|
|
// text += `Priority: ${summaryResponse.priority}.`;
|
|
// }
|
|
|
|
// return text;
|
|
// }
|
|
|
|
//************************************************* */
|
|
private async generateEmailWritingAssistance(writeAssistanceDto: WriteEmailAssistanceDto, _userId: string) {
|
|
const aiResponse = await this.aiService.assistEmailWriting(writeAssistanceDto.prompt);
|
|
|
|
return {
|
|
suggestion: {
|
|
subject: aiResponse.subject,
|
|
content: aiResponse.content,
|
|
alternatives: aiResponse.alternatives,
|
|
},
|
|
tips: aiResponse.tips,
|
|
improvements: aiResponse.improvements,
|
|
};
|
|
}
|
|
}
|