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

@@ -0,0 +1,14 @@
// src/rules-engine/types/action.types.ts
import type { RuleAction } from './rule.types';
export interface ActionHandler {
type: string;
execute(action: RuleAction, context: Record<string, any>): Promise<ActionResult>;
}
export type ActionResult = {
success: boolean;
data?: Record<string, any>;
error?: string;
};

View File

@@ -0,0 +1,15 @@
// src/rules-engine/types/fact.types.ts
export type FactValue = string | number | boolean | string[] | null;
export type FactContext = {
lead?: Record<string, FactValue>;
call?: Record<string, FactValue>;
agent?: Record<string, FactValue>;
campaign?: Record<string, FactValue>;
};
export interface FactProvider {
name: string;
resolveFacts(entityData: any): Promise<Record<string, FactValue>>;
}

View File

@@ -0,0 +1,126 @@
// src/rules-engine/types/rule.types.ts
export type RuleType = 'priority' | 'automation';
export type RuleTrigger =
| { type: 'on_request'; request: 'worklist' | 'assignment' }
| { type: 'on_event'; event: string }
| { type: 'on_schedule'; interval: string }
| { type: 'always' };
export type RuleCategory = 'priority' | 'assignment' | 'escalation' | 'lifecycle' | 'qualification';
export type RuleOperator =
| 'equal' | 'notEqual'
| 'greaterThan' | 'greaterThanInclusive'
| 'lessThan' | 'lessThanInclusive'
| 'in' | 'notIn'
| 'contains' | 'doesNotContain'
| 'exists' | 'doesNotExist';
export type RuleCondition = {
fact: string;
operator: RuleOperator;
value: any;
path?: string;
};
export type RuleConditionGroup = {
all?: (RuleCondition | RuleConditionGroup)[];
any?: (RuleCondition | RuleConditionGroup)[];
};
export type RuleActionType = 'score' | 'assign' | 'escalate' | 'update' | 'notify';
export type ScoreActionParams = {
weight: number;
slaMultiplier?: boolean;
campaignMultiplier?: boolean;
};
export type AssignActionParams = {
agentId?: string;
agentPool?: string[];
strategy: 'specific' | 'round-robin' | 'least-loaded' | 'skill-based';
};
export type EscalateActionParams = {
channel: 'toast' | 'notification' | 'sms' | 'email';
recipients: 'supervisor' | 'agent' | string[];
message: string;
severity: 'warning' | 'critical';
};
export type UpdateActionParams = {
entity: string;
field: string;
value: any;
};
export type RuleAction = {
type: RuleActionType;
params: ScoreActionParams | AssignActionParams | EscalateActionParams | UpdateActionParams;
};
export type Rule = {
id: string;
ruleType: RuleType;
name: string;
description?: string;
enabled: boolean;
priority: number;
trigger: RuleTrigger;
conditions: RuleConditionGroup;
action: RuleAction;
status?: 'draft' | 'published';
metadata: {
createdAt: string;
updatedAt: string;
createdBy: string;
category: RuleCategory;
tags?: string[];
};
};
export type ScoreBreakdown = {
baseScore: number;
slaMultiplier: number;
campaignMultiplier: number;
rulesApplied: string[];
};
export type ScoredItem = {
id: string;
score: number;
scoreBreakdown: ScoreBreakdown;
slaStatus: 'low' | 'medium' | 'high' | 'critical';
slaElapsedPercent: number;
};
// Priority config — what the supervisor edits via sliders
export type TaskWeightConfig = {
weight: number; // 0-10
slaMinutes: number; // SLA in minutes
enabled: boolean;
};
export type PriorityConfig = {
taskWeights: Record<string, TaskWeightConfig>;
campaignWeights: Record<string, number>; // campaignId → 0-10
sourceWeights: Record<string, number>; // leadSource → 0-10
};
export const DEFAULT_PRIORITY_CONFIG: PriorityConfig = {
taskWeights: {
missed_call: { weight: 9, slaMinutes: 720, enabled: true },
follow_up: { weight: 8, slaMinutes: 1440, enabled: true },
campaign_lead: { weight: 7, slaMinutes: 2880, enabled: true },
attempt_2: { weight: 6, slaMinutes: 1440, enabled: true },
attempt_3: { weight: 4, slaMinutes: 2880, enabled: true },
},
campaignWeights: {},
sourceWeights: {
WHATSAPP: 9, PHONE: 8, FACEBOOK_AD: 7, GOOGLE_AD: 7,
INSTAGRAM: 5, WEBSITE: 7, REFERRAL: 6, WALK_IN: 5, OTHER: 5,
},
};