mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
- Team module: POST /api/team/members (in-place employee creation with temp password + Redis cache), PUT /api/team/members/:id, GET temp password endpoint. Uses signUpInWorkspace — no email invites. - Dockerfile: rewritten as multi-stage build (builder + runtime) so native modules compile for target arch. Fixes darwin→linux crash. - .dockerignore: exclude dist, node_modules, .env, .git, data/ - package-lock.json: regenerated against public npmjs.org (was pointing at localhost:4873 Verdaccio — broke docker builds) - Doctor utils: shared DOCTOR_VISIT_SLOTS_FRAGMENT + normalizeDoctors helper for visit-slot-aware queries across 6 consumers - AI config: full admin CRUD (GET/PUT/POST reset), workspace-scoped setup-state with workspace ID isolation, AI prompt defaults overhaul - Agent config: camelCase field fix for SDK-synced workspaces - Session service: workspace-scoped Redis key prefixing for setup state - Recordings/supervisor/widget services: updated to use doctor-utils shared fragments instead of inline visitingHours queries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
114 lines
4.4 KiB
TypeScript
114 lines
4.4 KiB
TypeScript
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<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);
|
|
}
|
|
|
|
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 };
|
|
}
|
|
}
|