update: the ai interface and enum

This commit is contained in:
mahyargdz
2025-07-26 14:34:04 +03:30
parent a753badcef
commit 6b6566b982
9 changed files with 69 additions and 57 deletions
@@ -1,7 +1,7 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator"; import { IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator";
import { SummaryLength, SummaryTone } from "../enums/ai.enum"; import { SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
export class SummarizeEmailDto { export class SummarizeEmailDto {
@IsNotEmpty({ message: "Message ID is required" }) @IsNotEmpty({ message: "Message ID is required" })
@@ -15,12 +15,12 @@ export class SummarizeEmailDto {
mailbox: string; mailbox: string;
@IsOptional() @IsOptional()
@IsEnum(SummaryLength, { message: "Summary length must be one of: short, medium, detailed" }) @IsEnum(SummaryLengthEnum, { message: "Summary length must be one of: short, medium, detailed" })
@ApiPropertyOptional({ description: "Preferred summary length", enum: SummaryLength, default: SummaryLength.MEDIUM }) @ApiPropertyOptional({ description: "Preferred summary length", enum: SummaryLengthEnum, default: SummaryLengthEnum.MEDIUM })
length?: SummaryLength; length?: SummaryLengthEnum;
@IsOptional() @IsOptional()
@IsEnum(SummaryTone, { message: "Summary tone must be one of: formal, casual, professional" }) @IsEnum(SummaryToneEnum, { message: "Summary tone must be one of: formal, casual, professional" })
@ApiPropertyOptional({ description: "Preferred summary tone", enum: SummaryTone, default: SummaryTone.PROFESSIONAL }) @ApiPropertyOptional({ description: "Preferred summary tone", enum: SummaryToneEnum, default: SummaryToneEnum.PROFESSIONAL })
tone?: SummaryTone; tone?: SummaryToneEnum;
} }
+17 -5
View File
@@ -1,23 +1,35 @@
export enum SummaryLength { export enum SummaryLengthEnum {
SHORT = "short", SHORT = "short",
MEDIUM = "medium", MEDIUM = "medium",
DETAILED = "detailed", DETAILED = "detailed",
} }
export enum SummaryTone { export enum SentimentEnum {
POSITIVE = "positive",
NEUTRAL = "neutral",
NEGATIVE = "negative",
}
export enum LanguageEnum {
ENGLISH = "English",
PERSIAN = "Persian",
}
export enum SummaryToneEnum {
FORMAL = "formal", FORMAL = "formal",
CASUAL = "casual", CASUAL = "casual",
PROFESSIONAL = "professional", PROFESSIONAL = "professional",
} }
export enum EmailPurpose { export enum EmailPurposeEnum {
GENERAL = "general",
REPLY = "reply", REPLY = "reply",
FORWARD = "forward", FORWARD = "forward",
NEW = "new", NEW = "new",
FOLLOW_UP = "follow_up", FOLLOW_UP = "follow_up",
} }
export enum WritingTone { export enum WritingToneEnum {
FORMAL = "formal", FORMAL = "formal",
CASUAL = "casual", CASUAL = "casual",
PROFESSIONAL = "professional", PROFESSIONAL = "professional",
@@ -25,7 +37,7 @@ export enum WritingTone {
URGENT = "urgent", URGENT = "urgent",
} }
export enum EmailLength { export enum EmailLengthEnum {
SHORT = "short", SHORT = "short",
MEDIUM = "medium", MEDIUM = "medium",
LONG = "long", LONG = "long",
@@ -1,25 +1,29 @@
export interface EmailData { import { PriorityEnum } from "../../email/enums/email-header.enum";
import { MessageAttachment } from "../../mail-server/interfaces/messages-response.interface";
import { LanguageEnum, SentimentEnum, SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
export interface IEmailData {
subject: string; subject: string;
content: string; content: string;
htmlContent?: string; htmlContent?: string;
from: string; from: string;
to: string[]; to: string[];
date: Date; date: string;
attachments?: unknown[]; attachments?: boolean | MessageAttachment[];
} }
export interface SummarizationOptions { export interface SummarizationOptions {
length: "short" | "medium" | "detailed"; length: SummaryLengthEnum;
tone: "formal" | "casual" | "professional"; tone: SummaryToneEnum;
language?: "Persian" | "English"; language?: LanguageEnum;
} }
export interface AISummaryResponse { export interface AISummaryResponse {
summary: string; summary: string;
keyPoints: string[]; keyPoints: string[];
suggestedActions: string[]; suggestedActions: string[];
priority: "low" | "medium" | "high"; priority: PriorityEnum;
sentiment: "positive" | "neutral" | "negative"; sentiment: SentimentEnum;
} }
export interface AIWritingResponse { export interface AIWritingResponse {
@@ -31,6 +35,6 @@ export interface AIWritingResponse {
} }
export interface AIServiceInterface { export interface AIServiceInterface {
summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise<AISummaryResponse>; summarizeEmail(emailData: IEmailData, options: SummarizationOptions): Promise<AISummaryResponse>;
assistEmailWriting(prompt: string): Promise<AIWritingResponse>; assistEmailWriting(prompt: string): Promise<AIWritingResponse>;
} }
@@ -7,8 +7,8 @@ import { MessageDetails } from "../../mail-server/interfaces/messages-response.i
import { WildDuckMessagesService } from "../../mail-server/services/wildduck-messages.service"; import { WildDuckMessagesService } from "../../mail-server/services/wildduck-messages.service";
import { SummarizeEmailDto } from "../DTO/summarize-email.dto"; import { SummarizeEmailDto } from "../DTO/summarize-email.dto";
import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto"; import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto";
import { SummaryLength, SummaryTone } from "../enums/ai.enum"; import { SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
import { EmailData, SummarizationOptions } from "../interfaces/ai-service.interface"; import { IEmailData, SummarizationOptions } from "../interfaces/ai-service.interface";
@Injectable() @Injectable()
export class AiAssistantService { export class AiAssistantService {
@@ -45,7 +45,7 @@ export class AiAssistantService {
htmlContent: this.extractHtmlContent(message), htmlContent: this.extractHtmlContent(message),
from: this.extractSenderInfo(message), from: this.extractSenderInfo(message),
to: this.extractRecipientInfo(message), to: this.extractRecipientInfo(message),
date: message.date || new Date(), date: message.date || new Date().toISOString(),
attachments: message.attachments || [], attachments: message.attachments || [],
}; };
} }
@@ -99,12 +99,12 @@ export class AiAssistantService {
} }
//************************************************* */ //************************************************* */
private async generateEmailSummary(emailData: any, length: SummaryLength = SummaryLength.MEDIUM, _tone: SummaryTone = SummaryTone.PROFESSIONAL) { private async generateEmailSummary(emailData: IEmailData, length?: SummaryLengthEnum, tone?: SummaryToneEnum) {
const content = emailData.content || emailData.htmlContent; const content = emailData.content || emailData.htmlContent;
if (!content.trim()) throw new BadRequestException(AiMessage.EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED); if (!content || !content.trim()) throw new BadRequestException(AiMessage.EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED);
const aiEmailData: EmailData = { const aiEmailData: IEmailData = {
subject: emailData.subject, subject: emailData.subject,
content: emailData.content, content: emailData.content,
htmlContent: emailData.htmlContent, htmlContent: emailData.htmlContent,
@@ -115,8 +115,8 @@ export class AiAssistantService {
}; };
const summarizationOptions: SummarizationOptions = { const summarizationOptions: SummarizationOptions = {
length: length.toLowerCase() as "short" | "medium" | "detailed", length: length || SummaryLengthEnum.MEDIUM,
tone: _tone.toLowerCase() as "formal" | "casual" | "professional", tone: tone || SummaryToneEnum.PROFESSIONAL,
}; };
const aiResponse = await this.aiService.summarizeEmail(aiEmailData, summarizationOptions); const aiResponse = await this.aiService.summarizeEmail(aiEmailData, summarizationOptions);
@@ -4,15 +4,16 @@ import OpenAI from "openai";
import { AI_CONFIG } from "../../../common/constants"; import { AI_CONFIG } from "../../../common/constants";
import { AiMessage } from "../../../common/enums/message.enum"; import { AiMessage } from "../../../common/enums/message.enum";
import { AIConfig } from "../../../configs/ai.config"; import { AIConfig } from "../../../configs/ai.config";
import { AIServiceInterface, AISummaryResponse, AIWritingResponse, EmailData, SummarizationOptions } from "../interfaces/ai-service.interface"; import { EmailPurposeEnum, LanguageEnum, SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
import { AIServiceInterface, AISummaryResponse, AIWritingResponse, IEmailData, SummarizationOptions } from "../interfaces/ai-service.interface";
@Injectable() @Injectable()
export class AiService implements AIServiceInterface { export class AiService implements AIServiceInterface {
private readonly openai: OpenAI; private readonly openai: OpenAI;
private readonly language: "Persian" | "English" = "Persian"; private readonly language: LanguageEnum = LanguageEnum.PERSIAN;
private readonly defaultTone: "formal" | "casual" | "professional" = "professional"; private readonly defaultTone: SummaryToneEnum = SummaryToneEnum.PROFESSIONAL;
private readonly defaultLength: "short" | "medium" | "detailed" = "medium"; private readonly defaultLength: SummaryLengthEnum = SummaryLengthEnum.MEDIUM;
private readonly defaultPurpose: "general" | "reply" | "forward" | "new" | "follow_up" = "general"; private readonly defaultPurpose: EmailPurposeEnum = EmailPurposeEnum.GENERAL;
constructor(@Inject(AI_CONFIG) private readonly aiConfig: AIConfig) { constructor(@Inject(AI_CONFIG) private readonly aiConfig: AIConfig) {
this.openai = new OpenAI({ this.openai = new OpenAI({
@@ -23,7 +24,7 @@ export class AiService implements AIServiceInterface {
} }
//************************************************* */ //************************************************* */
async summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise<AISummaryResponse> { async summarizeEmail(emailData: IEmailData, options: SummarizationOptions): Promise<AISummaryResponse> {
const prompt = this.buildSummarizationPrompt(emailData, options); const prompt = this.buildSummarizationPrompt(emailData, options);
const completion = await this.openai.chat.completions.create({ const completion = await this.openai.chat.completions.create({
@@ -98,7 +99,7 @@ export class AiService implements AIServiceInterface {
} }
//************************************************* */ //************************************************* */
private buildSummarizationPrompt(emailData: EmailData, options: SummarizationOptions): string { private buildSummarizationPrompt(emailData: IEmailData, options: SummarizationOptions): string {
const template = this.aiConfig.farsiPrompts.summarization.promptTemplate; const template = this.aiConfig.farsiPrompts.summarization.promptTemplate;
let prompt = `${template.intro}\n\n`; let prompt = `${template.intro}\n\n`;
@@ -108,7 +109,7 @@ export class AiService implements AIServiceInterface {
prompt += `${template.dateLabel} ${emailData.date}\n\n`; prompt += `${template.dateLabel} ${emailData.date}\n\n`;
prompt += `${template.contentLabel}\n${emailData.content}\n\n`; prompt += `${template.contentLabel}\n${emailData.content}\n\n`;
if (emailData.attachments && emailData.attachments.length > 0) { if (emailData.attachments && Array.isArray(emailData.attachments) && emailData.attachments.length > 0) {
prompt += `${template.attachmentsLabel} ${emailData.attachments.length} فایل\n\n`; prompt += `${template.attachmentsLabel} ${emailData.attachments.length} فایل\n\n`;
} }
+1 -1
View File
@@ -34,7 +34,7 @@ export const enum EmailHeaderKey {
XTrustedSources = "X-Trusted-Sources", XTrustedSources = "X-Trusted-Sources",
} }
export enum Priority { export enum PriorityEnum {
High = "high", High = "high",
Normal = "normal", Normal = "normal",
Low = "low", Low = "low",
@@ -1,4 +1,4 @@
import { Priority } from "../enums/email-header.enum"; import { PriorityEnum } from "../enums/email-header.enum";
export interface IGenerateAntiThreadingHeadersOptions { export interface IGenerateAntiThreadingHeadersOptions {
messageId?: string; messageId?: string;
@@ -8,7 +8,7 @@ export interface IGenerateAntiThreadingHeadersOptions {
businessDomain: string; businessDomain: string;
isTransactional?: boolean; isTransactional?: boolean;
isMarketing?: boolean; isMarketing?: boolean;
priority?: Priority; priority?: PriorityEnum;
// Image-related options // Image-related options
hasHostedImages?: boolean; hasHostedImages?: boolean;
imageCount?: number; imageCount?: number;
@@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { EmailHeaderDto } from "../DTO/send-email.dto"; import { EmailHeaderDto } from "../DTO/send-email.dto";
import { EmailHeaderKey, Priority } from "../enums/email-header.enum"; import { EmailHeaderKey, PriorityEnum } from "../enums/email-header.enum";
import { import {
IGenerateAntiThreadingHeadersOptions, IGenerateAntiThreadingHeadersOptions,
IGenerateGmailAntiThreadingHeadersOptions, IGenerateGmailAntiThreadingHeadersOptions,
@@ -36,12 +36,12 @@ export class EmailHeadersService {
headers.push({ headers.push({
key: EmailHeaderKey.XPriority, key: EmailHeaderKey.XPriority,
value: this.getPriorityValue(options.priority || Priority.Normal), value: this.getPriorityValue(options.priority || PriorityEnum.Normal),
}); });
headers.push({ headers.push({
key: EmailHeaderKey.XMSMailPriority, key: EmailHeaderKey.XMSMailPriority,
value: options.priority || Priority.Normal, value: options.priority || PriorityEnum.Normal,
}); });
headers.push({ headers.push({
@@ -344,7 +344,7 @@ export class EmailHeadersService {
} }
//****************************************** */ //****************************************** */
private getPriorityValue(priority: Priority): string { private getPriorityValue(priority: PriorityEnum): string {
return priority === Priority.High ? "1" : priority === Priority.Low ? "5" : "3"; return priority === PriorityEnum.High ? "1" : priority === PriorityEnum.Low ? "5" : "3";
} }
} }
+7 -12
View File
@@ -15,7 +15,7 @@ import { UserRepository } from "../../users/repositories/user.repository";
import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto"; import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto";
import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto"; import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto";
import { EmailRecipientDto, EmailType, SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto"; import { EmailRecipientDto, EmailType, SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto";
import { Priority } from "../enums/email-header.enum"; import { PriorityEnum } from "../enums/email-header.enum";
import { IEmailMap } from "../interfaces/email-map.interface"; import { IEmailMap } from "../interfaces/email-map.interface";
@Injectable() @Injectable()
@@ -108,18 +108,12 @@ export class EmailService {
limit: query.limit || 20, limit: query.limit || 20,
page: query.page || 1, page: query.page || 1,
order: query.order || "desc", order: query.order || "desc",
mailbox: query.mailbox || "", query: query.search,
from: query.from || query.search,
// mailbox: query.mailbox,
}; };
delete wildDuckQuery.mailbox;
if (query.search) { delete wildDuckQuery?.mailbox;
wildDuckQuery.query = query.search;
wildDuckQuery.from = query.search;
}
if (query.from) wildDuckQuery.from = query.from;
if (query.to) wildDuckQuery.to = query.to;
if (query.subject) wildDuckQuery.subject = query.subject;
const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery)); const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery));
@@ -727,6 +721,7 @@ export class EmailService {
//============================================== //==============================================
private transformPaginationQuery(query: MessageListQueryDto): Record<string, unknown> { private transformPaginationQuery(query: MessageListQueryDto): Record<string, unknown> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const transformedQuery: Record<string, any> = { ...query }; const transformedQuery: Record<string, any> = { ...query };
if (!transformedQuery.page) transformedQuery.page = 1; if (!transformedQuery.page) transformedQuery.page = 1;
@@ -767,7 +762,7 @@ export class EmailService {
businessName, businessName,
businessDomain, businessDomain,
isTransactional: true, isTransactional: true,
priority: Priority.Normal, priority: PriorityEnum.Normal,
hasHostedImages: imageDetection.hasHostedImages, hasHostedImages: imageDetection.hasHostedImages,
imageCount: imageDetection.imageCount, imageCount: imageDetection.imageCount,
trustedImageSources: imageDetection.trustedImageSources, trustedImageSources: imageDetection.trustedImageSources,