Files
helix-engage-server/src/worklist/worklist.service.ts
saridsa2 b8556cf440 feat: rules engine — json-rules-engine integration with worklist scoring
- Self-contained NestJS module: types, storage (Redis+JSON), fact providers, action handlers
- PriorityConfig CRUD (slider values for task weights, campaign weights, source weights)
- Score action handler with SLA multiplier + campaign multiplier formula
- Worklist consumer: scores and ranks items before returning
- Hospital starter template (7 rules)
- REST API: /api/rules/* (CRUD, priority-config, evaluate, templates)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:59:10 +05:30

118 lines
4.9 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
import { WorklistConsumer } from '../rules-engine/consumers/worklist.consumer';
export type WorklistResponse = {
missedCalls: any[];
followUps: any[];
marketingLeads: any[];
totalPending: number;
};
@Injectable()
export class WorklistService {
private readonly logger = new Logger(WorklistService.name);
constructor(
private readonly platform: PlatformGraphqlService,
private readonly worklistConsumer: WorklistConsumer,
) {}
async getWorklist(agentName: string, authHeader: string): Promise<WorklistResponse> {
const [rawMissedCalls, rawFollowUps, rawMarketingLeads] = await Promise.all([
this.getMissedCalls(agentName, authHeader),
this.getPendingFollowUps(agentName, authHeader),
this.getAssignedLeads(agentName, authHeader),
]);
// Tag each item with a type field for the scoring engine
const combined = [
...rawMissedCalls.map((item: any) => ({ ...item, type: 'missed' })),
...rawFollowUps.map((item: any) => ({ ...item, type: 'follow-up' })),
...rawMarketingLeads.map((item: any) => ({ ...item, type: 'lead' })),
];
// Score and rank via rules engine
const scored = await this.worklistConsumer.scoreAndRank(combined);
// Split back into the 3 categories
const missedCalls = scored.filter((item: any) => item.type === 'missed');
const followUps = scored.filter((item: any) => item.type === 'follow-up');
const marketingLeads = scored.filter((item: any) => item.type === 'lead');
return {
missedCalls,
followUps,
marketingLeads,
totalPending: missedCalls.length + followUps.length + marketingLeads.length,
};
}
private async getAssignedLeads(agentName: string, authHeader: string): Promise<any[]> {
try {
const data = await this.platform.queryWithAuth<any>(
`{ leads(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }, orderBy: [{ createdAt: AscNullsLast }]) { edges { node {
id createdAt
contactName { firstName lastName }
contactPhone { primaryPhoneNumber }
contactEmail { primaryEmail }
source status interestedService
assignedAgent campaignId
contactAttempts spamScore isSpam
aiSummary aiSuggestedAction
} } } }`,
undefined,
authHeader,
);
return data.leads.edges.map((e: any) => e.node);
} catch (err) {
this.logger.warn(`Failed to fetch assigned leads: ${err}`);
return [];
}
}
private async getPendingFollowUps(agentName: string, authHeader: string): Promise<any[]> {
try {
const data = await this.platform.queryWithAuth<any>(
`{ followUps(first: 20, filter: { assignedAgent: { eq: "${agentName}" } }) { edges { node {
id name createdAt
typeCustom status scheduledAt completedAt
priority assignedAgent
patientId
} } } }`,
undefined,
authHeader,
);
// Filter to PENDING/OVERDUE client-side since platform may not support in-filter on remapped fields
return data.followUps.edges
.map((e: any) => e.node)
.filter((f: any) => f.status === 'PENDING' || f.status === 'OVERDUE');
} catch (err) {
this.logger.warn(`Failed to fetch follow-ups: ${err}`);
return [];
}
}
private async getMissedCalls(agentName: string, authHeader: string): Promise<any[]> {
try {
// FIFO ordering (AscNullsLast) — oldest first. No agentName filter — missed calls are a shared queue.
const data = await this.platform.queryWithAuth<any>(
`{ calls(first: 20, filter: { callStatus: { eq: MISSED }, callbackstatus: { in: [PENDING_CALLBACK, CALLBACK_ATTEMPTED] } }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node {
id name createdAt
direction callStatus agentName
callerNumber { primaryPhoneNumber }
startedAt endedAt durationSec
disposition leadId
callbackstatus callsourcenumber missedcallcount callbackattemptedat
} } } }`,
undefined,
authHeader,
);
return data.calls.edges.map((e: any) => e.node);
} catch (err) {
this.logger.warn(`Failed to fetch missed calls: ${err}`);
return [];
}
}
}