mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
- caller-resolution: drop cache, use indexed phone filter (lead.contactPhone.primaryPhoneNumber.like) - worklist: externalize page size (WORKLIST_PAGE_SIZE × WORKLIST_MAX_PAGES), paginate getMissedCalls/getAssignedLeads/getPendingFollowUps - maint: unlock-agent, force-ready, backfill-caller-resolution, clear-analysis-cache, fix-timestamps - ozonetel agent.service: force logout+re-login on "already logged in" - ai chat: context expansion - livekit-agent: updates - widget: session handling - masterdata: clinic list cache Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
115 lines
4.7 KiB
TypeScript
115 lines
4.7 KiB
TypeScript
import { Body, Controller, Headers, HttpException, Logger, Param, Post } from '@nestjs/common';
|
|
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
|
import { AiEnrichmentService } from '../ai/ai-enrichment.service';
|
|
import { CallerResolutionService } from '../caller/caller-resolution.service';
|
|
|
|
// POST /api/lead/:id/enrich
|
|
//
|
|
// Force re-generation of a lead's AI summary + suggested action. Used by
|
|
// the call-desk appointment/enquiry forms when the agent explicitly edits
|
|
// the caller's name — the previously-generated summary was built against
|
|
// the stale identity, so we discard it and run the enrichment prompt
|
|
// again with the corrected name.
|
|
//
|
|
// Optional body: `{ phone?: string }` — when provided, also invalidates
|
|
// the Redis caller-resolution cache for that phone so the NEXT incoming
|
|
// call from the same number picks up fresh data from the platform
|
|
// instead of the stale cached entry.
|
|
//
|
|
// This is distinct from the cache-miss enrichment path in
|
|
// call-lookup.controller.ts `POST /api/call/lookup` which only runs
|
|
// enrichment when `lead.aiSummary` is null. That path is fine for
|
|
// first-time lookups; this one is for explicit "the old summary is
|
|
// wrong, regenerate it" triggers.
|
|
@Controller('api/lead')
|
|
export class LeadEnrichController {
|
|
private readonly logger = new Logger(LeadEnrichController.name);
|
|
|
|
constructor(
|
|
private readonly platform: PlatformGraphqlService,
|
|
private readonly ai: AiEnrichmentService,
|
|
private readonly callerResolution: CallerResolutionService,
|
|
) {}
|
|
|
|
@Post(':id/enrich')
|
|
async enrichLead(
|
|
@Param('id') leadId: string,
|
|
@Body() body: { phone?: string },
|
|
@Headers('authorization') authHeader: string,
|
|
) {
|
|
if (!authHeader) throw new HttpException('Authorization required', 401);
|
|
if (!leadId) throw new HttpException('leadId required', 400);
|
|
|
|
this.logger.log(`Force-enriching lead ${leadId}`);
|
|
|
|
// 1. Fetch fresh lead from platform (with the staging-aligned
|
|
// field names — see findLeadByIdWithToken comment).
|
|
let lead: any;
|
|
try {
|
|
lead = await this.platform.findLeadByIdWithToken(leadId, authHeader);
|
|
} catch (err) {
|
|
this.logger.error(`[LEAD-ENRICH] Lead fetch failed for ${leadId}: ${err}`);
|
|
throw new HttpException(`Lead fetch failed: ${(err as Error).message}`, 500);
|
|
}
|
|
if (!lead) {
|
|
throw new HttpException(`Lead not found: ${leadId}`, 404);
|
|
}
|
|
|
|
// 2. Fetch recent activities so the prompt has conversation context.
|
|
let activities: any[] = [];
|
|
try {
|
|
activities = await this.platform.getLeadActivitiesWithToken(leadId, authHeader, 5);
|
|
} catch (err) {
|
|
// Non-fatal — enrichment just has less context.
|
|
this.logger.warn(`[LEAD-ENRICH] Activity fetch failed: ${err}`);
|
|
}
|
|
|
|
// 3. Run enrichment. LeadContext uses the legacy `leadStatus`/
|
|
// `leadSource` internal names even though the platform now
|
|
// exposes them as `status`/`source` — we just map across.
|
|
const enrichment = await this.ai.enrichLead({
|
|
firstName: lead.contactName?.firstName ?? undefined,
|
|
lastName: lead.contactName?.lastName ?? undefined,
|
|
leadSource: lead.source ?? undefined,
|
|
interestedService: lead.interestedService ?? undefined,
|
|
leadStatus: lead.status ?? undefined,
|
|
contactAttempts: lead.contactAttempts ?? undefined,
|
|
createdAt: lead.createdAt,
|
|
activities: activities.map((a) => ({
|
|
activityType: a.activityType ?? '',
|
|
summary: a.summary ?? '',
|
|
})),
|
|
});
|
|
|
|
// 4. Persist the new summary back to the lead.
|
|
try {
|
|
await this.platform.updateLeadWithToken(
|
|
leadId,
|
|
{
|
|
aiSummary: enrichment.aiSummary,
|
|
aiSuggestedAction: enrichment.aiSuggestedAction,
|
|
},
|
|
authHeader,
|
|
);
|
|
} catch (err) {
|
|
this.logger.error(`[LEAD-ENRICH] Failed to persist enrichment for ${leadId}: ${err}`);
|
|
throw new HttpException(
|
|
`Failed to persist enrichment: ${(err as Error).message}`,
|
|
500,
|
|
);
|
|
}
|
|
|
|
// Caller resolution no longer caches — every resolve() hits the
|
|
// platform fresh via an indexed phone filter. No invalidation
|
|
// needed after enrichment.
|
|
|
|
this.logger.log(`[LEAD-ENRICH] Lead ${leadId} enriched successfully`);
|
|
|
|
return {
|
|
leadId,
|
|
aiSummary: enrichment.aiSummary,
|
|
aiSuggestedAction: enrichment.aiSuggestedAction,
|
|
};
|
|
}
|
|
}
|