mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
feat: add Exotel webhook controller and service for call event parsing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
113
src/ai/ai-enrichment.service.ts
Normal file
113
src/ai/ai-enrichment.service.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AiEnrichmentService {
|
||||
private readonly logger = new Logger(AiEnrichmentService.name);
|
||||
private client: Anthropic | null = null;
|
||||
|
||||
constructor(private config: ConfigService) {
|
||||
const apiKey = config.get<string>('ai.anthropicApiKey');
|
||||
if (apiKey) {
|
||||
this.client = new Anthropic({ apiKey });
|
||||
} else {
|
||||
this.logger.warn('ANTHROPIC_API_KEY not set — AI enrichment disabled, using fallback');
|
||||
}
|
||||
}
|
||||
|
||||
async enrichLead(lead: LeadContext): Promise<EnrichmentResult> {
|
||||
// Fallback if no API key configured
|
||||
if (!this.client) {
|
||||
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 prompt = `You are an AI assistant for a hospital call center at Global Hospital.
|
||||
An inbound call is coming in from a lead. Summarize their history and suggest what the call center agent should do.
|
||||
|
||||
Lead details:
|
||||
- Name: ${lead.firstName ?? 'Unknown'} ${lead.lastName ?? ''}
|
||||
- Source: ${lead.leadSource ?? 'Unknown'}
|
||||
- Interested in: ${lead.interestedService ?? 'Unknown'}
|
||||
- Current status: ${lead.leadStatus ?? 'Unknown'}
|
||||
- Lead age: ${daysSince} days
|
||||
- Contact attempts: ${lead.contactAttempts ?? 0}
|
||||
|
||||
Recent activity:
|
||||
${activitiesText}
|
||||
|
||||
Respond ONLY with valid JSON (no markdown, no code blocks):
|
||||
{"aiSummary": "1-2 sentence summary of who this lead is and their history", "aiSuggestedAction": "5-10 word suggested action for the agent"}`;
|
||||
|
||||
const response = await this.client.messages.create({
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
max_tokens: 200,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
||||
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
||||
const parsed = JSON.parse(text.trim());
|
||||
|
||||
this.logger.log(`AI enrichment generated for lead ${lead.firstName} ${lead.lastName}`);
|
||||
return {
|
||||
aiSummary: parsed.aiSummary,
|
||||
aiSuggestedAction: parsed.aiSuggestedAction,
|
||||
};
|
||||
} 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user