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>
This commit is contained in:
2026-04-01 16:59:10 +05:30
parent 7b59543d36
commit b8556cf440
20 changed files with 959 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
import { Module, forwardRef } from '@nestjs/common';
import { PlatformModule } from '../platform/platform.module';
import { OzonetelAgentModule } from '../ozonetel/ozonetel-agent.module';
import { RulesEngineModule } from '../rules-engine/rules-engine.module';
import { WorklistController } from './worklist.controller';
import { WorklistService } from './worklist.service';
import { MissedQueueService } from './missed-queue.service';
@@ -8,7 +9,7 @@ import { MissedCallWebhookController } from './missed-call-webhook.controller';
import { KookooCallbackController } from './kookoo-callback.controller';
@Module({
imports: [PlatformModule, forwardRef(() => OzonetelAgentModule)],
imports: [PlatformModule, forwardRef(() => OzonetelAgentModule), RulesEngineModule],
controllers: [WorklistController, MissedCallWebhookController, KookooCallbackController],
providers: [WorklistService, MissedQueueService],
exports: [MissedQueueService],

View File

@@ -1,5 +1,6 @@
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[];
@@ -12,15 +13,33 @@ export type WorklistResponse = {
export class WorklistService {
private readonly logger = new Logger(WorklistService.name);
constructor(private readonly platform: PlatformGraphqlService) {}
constructor(
private readonly platform: PlatformGraphqlService,
private readonly worklistConsumer: WorklistConsumer,
) {}
async getWorklist(agentName: string, authHeader: string): Promise<WorklistResponse> {
const [missedCalls, followUps, marketingLeads] = await Promise.all([
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,