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, }; } }