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 { 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 {
@IsNotEmpty({ message: "Message ID is required" })
@@ -15,12 +15,12 @@ export class SummarizeEmailDto {
mailbox: string;
@IsOptional()
@IsEnum(SummaryLength, { message: "Summary length must be one of: short, medium, detailed" })
@ApiPropertyOptional({ description: "Preferred summary length", enum: SummaryLength, default: SummaryLength.MEDIUM })
length?: SummaryLength;
@IsEnum(SummaryLengthEnum, { message: "Summary length must be one of: short, medium, detailed" })
@ApiPropertyOptional({ description: "Preferred summary length", enum: SummaryLengthEnum, default: SummaryLengthEnum.MEDIUM })
length?: SummaryLengthEnum;
@IsOptional()
@IsEnum(SummaryTone, { message: "Summary tone must be one of: formal, casual, professional" })
@ApiPropertyOptional({ description: "Preferred summary tone", enum: SummaryTone, default: SummaryTone.PROFESSIONAL })
tone?: SummaryTone;
@IsEnum(SummaryToneEnum, { message: "Summary tone must be one of: formal, casual, professional" })
@ApiPropertyOptional({ description: "Preferred summary tone", enum: SummaryToneEnum, default: SummaryToneEnum.PROFESSIONAL })
tone?: SummaryToneEnum;
}
+17 -5
View File
@@ -1,23 +1,35 @@
export enum SummaryLength {
export enum SummaryLengthEnum {
SHORT = "short",
MEDIUM = "medium",
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",
CASUAL = "casual",
PROFESSIONAL = "professional",
}
export enum EmailPurpose {
export enum EmailPurposeEnum {
GENERAL = "general",
REPLY = "reply",
FORWARD = "forward",
NEW = "new",
FOLLOW_UP = "follow_up",
}
export enum WritingTone {
export enum WritingToneEnum {
FORMAL = "formal",
CASUAL = "casual",
PROFESSIONAL = "professional",
@@ -25,7 +37,7 @@ export enum WritingTone {
URGENT = "urgent",
}
export enum EmailLength {
export enum EmailLengthEnum {
SHORT = "short",
MEDIUM = "medium",
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;
content: string;
htmlContent?: string;
from: string;
to: string[];
date: Date;
attachments?: unknown[];
date: string;
attachments?: boolean | MessageAttachment[];
}
export interface SummarizationOptions {
length: "short" | "medium" | "detailed";
tone: "formal" | "casual" | "professional";
language?: "Persian" | "English";
length: SummaryLengthEnum;
tone: SummaryToneEnum;
language?: LanguageEnum;
}
export interface AISummaryResponse {
summary: string;
keyPoints: string[];
suggestedActions: string[];
priority: "low" | "medium" | "high";
sentiment: "positive" | "neutral" | "negative";
priority: PriorityEnum;
sentiment: SentimentEnum;
}
export interface AIWritingResponse {
@@ -31,6 +35,6 @@ export interface AIWritingResponse {
}
export interface AIServiceInterface {
summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise<AISummaryResponse>;
summarizeEmail(emailData: IEmailData, options: SummarizationOptions): Promise<AISummaryResponse>;
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 { SummarizeEmailDto } from "../DTO/summarize-email.dto";
import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto";
import { SummaryLength, SummaryTone } from "../enums/ai.enum";
import { EmailData, SummarizationOptions } from "../interfaces/ai-service.interface";
import { SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
import { IEmailData, SummarizationOptions } from "../interfaces/ai-service.interface";
@Injectable()
export class AiAssistantService {
@@ -45,7 +45,7 @@ export class AiAssistantService {
htmlContent: this.extractHtmlContent(message),
from: this.extractSenderInfo(message),
to: this.extractRecipientInfo(message),
date: message.date || new Date(),
date: message.date || new Date().toISOString(),
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;
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,
content: emailData.content,
htmlContent: emailData.htmlContent,
@@ -115,8 +115,8 @@ export class AiAssistantService {
};
const summarizationOptions: SummarizationOptions = {
length: length.toLowerCase() as "short" | "medium" | "detailed",
tone: _tone.toLowerCase() as "formal" | "casual" | "professional",
length: length || SummaryLengthEnum.MEDIUM,
tone: tone || SummaryToneEnum.PROFESSIONAL,
};
const aiResponse = await this.aiService.summarizeEmail(aiEmailData, summarizationOptions);
@@ -4,15 +4,16 @@ import OpenAI from "openai";
import { AI_CONFIG } from "../../../common/constants";
import { AiMessage } from "../../../common/enums/message.enum";
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()
export class AiService implements AIServiceInterface {
private readonly openai: OpenAI;
private readonly language: "Persian" | "English" = "Persian";
private readonly defaultTone: "formal" | "casual" | "professional" = "professional";
private readonly defaultLength: "short" | "medium" | "detailed" = "medium";
private readonly defaultPurpose: "general" | "reply" | "forward" | "new" | "follow_up" = "general";
private readonly language: LanguageEnum = LanguageEnum.PERSIAN;
private readonly defaultTone: SummaryToneEnum = SummaryToneEnum.PROFESSIONAL;
private readonly defaultLength: SummaryLengthEnum = SummaryLengthEnum.MEDIUM;
private readonly defaultPurpose: EmailPurposeEnum = EmailPurposeEnum.GENERAL;
constructor(@Inject(AI_CONFIG) private readonly aiConfig: AIConfig) {
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 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;
let prompt = `${template.intro}\n\n`;
@@ -108,7 +109,7 @@ export class AiService implements AIServiceInterface {
prompt += `${template.dateLabel} ${emailData.date}\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`;
}
+1 -1
View File
@@ -34,7 +34,7 @@ export const enum EmailHeaderKey {
XTrustedSources = "X-Trusted-Sources",
}
export enum Priority {
export enum PriorityEnum {
High = "high",
Normal = "normal",
Low = "low",
@@ -1,4 +1,4 @@
import { Priority } from "../enums/email-header.enum";
import { PriorityEnum } from "../enums/email-header.enum";
export interface IGenerateAntiThreadingHeadersOptions {
messageId?: string;
@@ -8,7 +8,7 @@ export interface IGenerateAntiThreadingHeadersOptions {
businessDomain: string;
isTransactional?: boolean;
isMarketing?: boolean;
priority?: Priority;
priority?: PriorityEnum;
// Image-related options
hasHostedImages?: boolean;
imageCount?: number;
@@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common";
import { v4 as uuidv4 } from "uuid";
import { EmailHeaderDto } from "../DTO/send-email.dto";
import { EmailHeaderKey, Priority } from "../enums/email-header.enum";
import { EmailHeaderKey, PriorityEnum } from "../enums/email-header.enum";
import {
IGenerateAntiThreadingHeadersOptions,
IGenerateGmailAntiThreadingHeadersOptions,
@@ -36,12 +36,12 @@ export class EmailHeadersService {
headers.push({
key: EmailHeaderKey.XPriority,
value: this.getPriorityValue(options.priority || Priority.Normal),
value: this.getPriorityValue(options.priority || PriorityEnum.Normal),
});
headers.push({
key: EmailHeaderKey.XMSMailPriority,
value: options.priority || Priority.Normal,
value: options.priority || PriorityEnum.Normal,
});
headers.push({
@@ -344,7 +344,7 @@ export class EmailHeadersService {
}
//****************************************** */
private getPriorityValue(priority: Priority): string {
return priority === Priority.High ? "1" : priority === Priority.Low ? "5" : "3";
private getPriorityValue(priority: PriorityEnum): string {
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 { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.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";
@Injectable()
@@ -108,18 +108,12 @@ export class EmailService {
limit: query.limit || 20,
page: query.page || 1,
order: query.order || "desc",
mailbox: query.mailbox || "",
query: query.search,
from: query.from || query.search,
// mailbox: query.mailbox,
};
delete wildDuckQuery.mailbox;
if (query.search) {
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;
delete wildDuckQuery?.mailbox;
const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery));
@@ -727,6 +721,7 @@ export class EmailService {
//==============================================
private transformPaginationQuery(query: MessageListQueryDto): Record<string, unknown> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const transformedQuery: Record<string, any> = { ...query };
if (!transformedQuery.page) transformedQuery.page = 1;
@@ -767,7 +762,7 @@ export class EmailService {
businessName,
businessDomain,
isTransactional: true,
priority: Priority.Normal,
priority: PriorityEnum.Normal,
hasHostedImages: imageDetection.hasHostedImages,
imageCount: imageDetection.imageCount,
trustedImageSources: imageDetection.trustedImageSources,