import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { generateObject } from 'ai'; import { z } from 'zod'; import { EventBusService } from '../event-bus.service'; import { Topics } from '../event-types'; import type { CallCompletedEvent } from '../event-types'; import { PlatformGraphqlService } from '../../platform/platform-graphql.service'; import { createAiModel } from '../../ai/ai-provider'; import type { LanguageModel } from 'ai'; import { AiConfigService } from '../../config/ai-config.service'; @Injectable() export class AiInsightConsumer implements OnModuleInit { private readonly logger = new Logger(AiInsightConsumer.name); private readonly aiModel: LanguageModel | null; constructor( private eventBus: EventBusService, private platform: PlatformGraphqlService, 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'), }); } onModuleInit() { this.eventBus.on(Topics.CALL_COMPLETED, (event: CallCompletedEvent) => this.handleCallCompleted(event)); } private async handleCallCompleted(event: CallCompletedEvent): Promise { if (!event.leadId) { this.logger.debug('[AI-INSIGHT] No leadId — skipping'); return; } if (!this.aiModel) { this.logger.debug('[AI-INSIGHT] No AI model configured — skipping'); return; } this.logger.log(`[AI-INSIGHT] Generating insight for lead ${event.leadId}`); try { // Fetch lead + all activities const data = await this.platform.query( `{ leads(filter: { id: { eq: "${event.leadId}" } }) { edges { node { id name contactName { firstName lastName } status source interestedService contactAttempts lastContacted } } } }`, ); const lead = data?.leads?.edges?.[0]?.node; if (!lead) return; const activityData = await this.platform.query( `{ leadActivities(first: 20, filter: { leadId: { eq: "${event.leadId}" } }, orderBy: [{ occurredAt: DescNullsLast }]) { edges { node { activityType summary occurredAt channel durationSec outcome } } } }`, ); const activities = activityData?.leadActivities?.edges?.map((e: any) => e.node) ?? []; const leadName = lead.contactName ? `${lead.contactName.firstName ?? ''} ${lead.contactName.lastName ?? ''}`.trim() : lead.name ?? 'Unknown'; // Build context const activitySummary = activities.map((a: any) => `${a.activityType}: ${a.summary} (${a.occurredAt ?? 'unknown date'})`, ).join('\n'); // Generate insight const { object } = await generateObject({ model: this.aiModel, schema: z.object({ summary: z.string().describe('2-3 sentence summary of this lead based on all their interactions'), suggestedAction: z.string().describe('One clear next action for the agent'), }), system: this.aiConfig.renderPrompt('callInsight', { hospitalName: process.env.HOSPITAL_NAME ?? 'the hospital', }), prompt: `Lead: ${leadName} Status: ${lead.status ?? 'Unknown'} Source: ${lead.source ?? 'Unknown'} Interested in: ${lead.interestedService ?? 'Not specified'} Contact attempts: ${lead.contactAttempts ?? 0} Last contacted: ${lead.lastContacted ?? 'Never'} Recent activity (newest first): ${activitySummary || 'No activity recorded'} Latest call: - Direction: ${event.direction} - Duration: ${event.durationSec}s - Disposition: ${event.disposition} - Notes: ${event.notes ?? 'None'}`, maxOutputTokens: 200, }); // Update lead with new AI insight await this.platform.query( `mutation($id: UUID!, $data: LeadUpdateInput!) { updateLead(id: $id, data: $data) { id } }`, { id: event.leadId, data: { aiSummary: object.summary, aiSuggestedAction: object.suggestedAction, lastContacted: new Date().toISOString(), contactAttempts: (lead.contactAttempts ?? 0) + 1, }, }, ); this.logger.log(`[AI-INSIGHT] Updated lead ${event.leadId}: "${object.summary.substring(0, 60)}..."`); } catch (err: any) { this.logger.error(`[AI-INSIGHT] Failed for lead ${event.leadId}: ${err.message}`); } } }