mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
AI Summary not showing appointments fix.
This commit is contained in:
@@ -7,107 +7,149 @@ 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 }[];
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
leadSource?: string;
|
||||
interestedService?: string;
|
||||
leadStatus?: string;
|
||||
contactAttempts?: number;
|
||||
createdAt?: string;
|
||||
campaignId?: string;
|
||||
patient?: {
|
||||
age?: number;
|
||||
type?: string; // 'new' | 'returning' | 'vip'
|
||||
hasRecords?: boolean;
|
||||
};
|
||||
upcomingAppointments?: Array<{
|
||||
scheduledAt?: string;
|
||||
doctorName?: string;
|
||||
appointmentType?: string;
|
||||
status?: string;
|
||||
}>;
|
||||
activities?: { activityType: string; summary: string }[];
|
||||
};
|
||||
|
||||
type EnrichmentResult = {
|
||||
aiSummary: string;
|
||||
aiSuggestedAction: string;
|
||||
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'),
|
||||
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;
|
||||
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<string>('ai.anthropicApiKey'),
|
||||
openaiApiKey: config.get<string>('ai.openaiApiKey'),
|
||||
});
|
||||
if (!this.aiModel) {
|
||||
this.logger.warn('AI not configured — enrichment uses fallback');
|
||||
}
|
||||
constructor(
|
||||
private config: ConfigService,
|
||||
private aiConfig: AiConfigService,
|
||||
) {
|
||||
const cfg = aiConfig.getConfig();
|
||||
this.aiModel = createAiModel({
|
||||
provider: cfg.provider,
|
||||
model: cfg.model,
|
||||
anthropicApiKey: config.get<string>('ai.anthropicApiKey'),
|
||||
openaiApiKey: config.get<string>('ai.openaiApiKey'),
|
||||
});
|
||||
if (!this.aiModel) {
|
||||
this.logger.warn('AI not configured — enrichment uses fallback');
|
||||
}
|
||||
}
|
||||
|
||||
async enrichLead(lead: LeadContext): Promise<EnrichmentResult> {
|
||||
if (!this.aiModel) {
|
||||
return this.fallbackEnrichment(lead);
|
||||
}
|
||||
|
||||
async enrichLead(lead: LeadContext): Promise<EnrichmentResult> {
|
||||
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;
|
||||
|
||||
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 activitiesText = lead.activities?.length
|
||||
? lead.activities.map(a => `- ${a.activityType}: ${a.summary}`).join('\n')
|
||||
: 'No previous interactions';
|
||||
const patientContext = lead.patient
|
||||
? `Age: ${lead.patient.age ?? 'Unknown'} | Type: ${lead.patient.type ?? 'Unknown'} | Prior Records: ${lead.patient.hasRecords ? 'Yes' : 'No'}`
|
||||
: 'No patient record linked';
|
||||
|
||||
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,
|
||||
}),
|
||||
});
|
||||
const appointmentContext = lead.upcomingAppointments?.length
|
||||
? lead.upcomingAppointments
|
||||
.map(
|
||||
(a) =>
|
||||
`- ${a.scheduledAt ? new Date(a.scheduledAt).toLocaleDateString() : 'TBD'}: ${a.appointmentType ?? 'Appointment'} with ${a.doctorName ?? 'Doctor'} (${a.status ?? 'Scheduled'})`,
|
||||
)
|
||||
.join('\n')
|
||||
: 'No upcoming appointments';
|
||||
|
||||
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);
|
||||
}
|
||||
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,
|
||||
patientContext,
|
||||
appointmentContext,
|
||||
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`;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
return { aiSummary: summary, aiSuggestedAction: action };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user