import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { generateObject } from 'ai'; import type { LanguageModel } from 'ai'; import { z } from 'zod'; import { createAiModel } from './ai-provider'; import { AiConfigService } from '../config/ai-config.service'; type LeadContext = { firstName?: string; lastName?: string; leadSource?: string; interestedService?: string; leadStatus?: string; contactAttempts?: number; createdAt?: string; campaignId?: string; activities?: { activityType: string; summary: string }[]; }; type EnrichmentResult = { aiSummary: string; aiSuggestedAction: string; }; const enrichmentSchema = z.object({ aiSummary: z.string().describe('1-2 sentence summary of who this lead is and their history'), aiSuggestedAction: z.string().describe('5-10 word suggested action for the agent'), }); @Injectable() export class AiEnrichmentService { private readonly logger = new Logger(AiEnrichmentService.name); private readonly aiModel: LanguageModel | null; constructor( private config: ConfigService, private aiConfig: AiConfigService, ) { const cfg = aiConfig.getConfig(); this.aiModel = createAiModel({ provider: cfg.provider, model: cfg.model, anthropicApiKey: config.get('ai.anthropicApiKey'), openaiApiKey: config.get('ai.openaiApiKey'), }); if (!this.aiModel) { this.logger.warn('AI not configured — enrichment uses fallback'); } } async enrichLead(lead: LeadContext): Promise { if (!this.aiModel) { return this.fallbackEnrichment(lead); } try { const daysSince = lead.createdAt ? Math.floor((Date.now() - new Date(lead.createdAt).getTime()) / (1000 * 60 * 60 * 24)) : 0; const activitiesText = lead.activities?.length ? lead.activities.map(a => `- ${a.activityType}: ${a.summary}`).join('\n') : 'No previous interactions'; const { object } = await generateObject({ model: this.aiModel!, schema: enrichmentSchema, prompt: this.aiConfig.renderPrompt('leadEnrichment', { leadName: `${lead.firstName ?? 'Unknown'} ${lead.lastName ?? ''}`.trim(), leadSource: lead.leadSource ?? 'Unknown', interestedService: lead.interestedService ?? 'Unknown', leadStatus: lead.leadStatus ?? 'Unknown', daysSince, contactAttempts: lead.contactAttempts ?? 0, activities: activitiesText, }), }); this.logger.log(`AI enrichment generated for lead ${lead.firstName} ${lead.lastName}`); return object; } catch (error) { this.logger.error(`AI enrichment failed: ${error}`); return this.fallbackEnrichment(lead); } } private fallbackEnrichment(lead: LeadContext): EnrichmentResult { const daysSince = lead.createdAt ? Math.floor((Date.now() - new Date(lead.createdAt).getTime()) / (1000 * 60 * 60 * 24)) : 0; const attempts = lead.contactAttempts ?? 0; const service = lead.interestedService ?? 'general inquiry'; const source = lead.leadSource?.replace(/_/g, ' ').toLowerCase() ?? 'unknown source'; let summary: string; let action: string; if (attempts === 0) { summary = `First-time inquiry from ${source}. Interested in ${service}. Lead is ${daysSince} day(s) old with no previous contact.`; action = `Introduce services and offer appointment booking`; } else if (attempts === 1) { summary = `Returning inquiry — contacted once before. Interested in ${service} via ${source}. Lead is ${daysSince} day(s) old.`; action = `Follow up on previous conversation, offer appointment`; } else { summary = `Repeat contact (${attempts} previous attempts) for ${service}. Originally from ${source}, ${daysSince} day(s) old. Shows high interest.`; action = `Prioritize appointment booking — high-intent lead`; } return { aiSummary: summary, aiSuggestedAction: action }; } }