mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
- Rules engine spec v2 (priority vs automation rules distinction) - Priority Rules settings page with weight sliders, SLA config, campaign/source weights - Collapsible config sections with dynamic headers - Live worklist preview panel with client-side scoring - AI assistant panel (collapsible) with rules-engine-specific system prompt - Worklist panel: score display with SLA status dots, sort by score - Scoring library (scoring.ts) for client-side preview computation - Sidebar: Rules Engine nav item under Configuration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
433 lines
15 KiB
Markdown
433 lines
15 KiB
Markdown
# Rules Engine — Design Spec (v2)
|
||
|
||
**Date**: 2026-03-31 (revised 2026-04-01)
|
||
**Status**: Approved
|
||
**Phase**: 1 (Engine + Storage + API + Priority Rules UI + Worklist Integration)
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
A configurable rules engine that governs how leads flow through the hospital's call center — which leads get called first, which agent handles them, when to escalate, and when to mark them lost. Each hospital defines its own rules. No code changes needed to change behavior.
|
||
|
||
**Product pitch**: "Your hospital defines the rules, the call center follows them automatically."
|
||
|
||
---
|
||
|
||
## Two Rule Types
|
||
|
||
The engine supports two categories of rules, each with different behavior and UI:
|
||
|
||
### Priority Rules — "Who gets called first?"
|
||
- Configures worklist ranking via weights, SLA curves, campaign modifiers
|
||
- **Computed at request time** — scores are ephemeral, not persisted to entities
|
||
- Time-sensitive (SLA elapsed changes every minute — can't be persisted)
|
||
- Supervisor sees: weight sliders, SLA thresholds, campaign weights, live worklist preview
|
||
- No draft/publish needed — changes affect ranking immediately
|
||
|
||
### Automation Rules — "What should happen automatically?"
|
||
- Triggers durable actions when conditions are met: field updates, assignments, notifications
|
||
- **Writes back to entities** via platform GraphQL mutations (e.g., set lead.priority = HIGH)
|
||
- Event-driven (fires on lead.created, call.missed, etc.) or scheduled (every 5m)
|
||
- Supervisor sees: if-this-then-that condition builder with entity/field selectors
|
||
- **Draft/publish workflow** — rules don't affect live data until published
|
||
- Sub-types: Assignment, Escalation, Lifecycle
|
||
|
||
| Aspect | Priority Rules | Automation Rules |
|
||
|---|---|---|
|
||
| When | On worklist request | On entity event / on schedule |
|
||
| Effect | Ephemeral score for ranking | Durable entity mutation |
|
||
| Persisted? | No (recomputed each request) | Yes (writes to platform) |
|
||
| Draft/publish? | No (immediate) | Yes |
|
||
| UI | Sliders + live preview | Condition builder + draft/publish |
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
Self-contained NestJS module inside helix-engage-server (sidecar). Designed for extraction into a standalone microservice when needed.
|
||
|
||
```
|
||
helix-engage-server/src/rules-engine/
|
||
├── rules-engine.module.ts # NestJS module (self-contained)
|
||
├── rules-engine.service.ts # Core: json-rules-engine wrapper
|
||
├── rules-engine.controller.ts # REST API: CRUD + evaluate + config
|
||
├── rules-storage.service.ts # Redis (hot) + JSON file (backup)
|
||
├── types/
|
||
│ ├── rule.types.ts # Rule schema (priority + automation)
|
||
│ ├── fact.types.ts # Fact definitions + computed facts
|
||
│ └── action.types.ts # Action handler interface
|
||
├── facts/
|
||
│ ├── lead-facts.provider.ts # Lead/campaign data facts
|
||
│ ├── call-facts.provider.ts # Call/SLA data facts (+ computed: ageMinutes, slaElapsed)
|
||
│ └── agent-facts.provider.ts # Agent availability facts
|
||
├── actions/
|
||
│ ├── score.action.ts # Priority scoring action
|
||
│ ├── assign.action.ts # Lead-to-agent assignment (stub)
|
||
│ ├── escalate.action.ts # SLA breach alerts (stub)
|
||
│ └── update.action.ts # Update entity field (stub)
|
||
├── consumers/
|
||
│ └── worklist.consumer.ts # Applies scoring rules to worklist
|
||
└── templates/
|
||
└── hospital-starter.json # Pre-built rule set for new hospitals
|
||
```
|
||
|
||
### Dependencies
|
||
- `json-rules-engine` (npm) — rule evaluation
|
||
- Redis — active rule storage, score cache
|
||
- Platform GraphQL — fact data (leads, calls, campaigns, agents)
|
||
- No imports from other sidecar modules except via constructor injection
|
||
|
||
### Communication
|
||
- Own Redis namespace: `rules:*`
|
||
- Own route prefix: `/api/rules/*`
|
||
- Other modules call `RulesEngineService.evaluate()` — they don't import internals
|
||
|
||
---
|
||
|
||
## Fact System
|
||
|
||
### Design Principle: Entity-Driven Facts
|
||
Facts should ultimately be driven by entity metadata from the platform — adding a field to an entity automatically makes it available as a fact. This is the long-term goal.
|
||
|
||
### Phase 1: Curated Facts + Computed Facts
|
||
For Phase 1, facts are curated (hardcoded providers) with two categories:
|
||
|
||
**Entity field facts** — direct field values from platform entities:
|
||
- `lead.source`, `lead.status`, `lead.campaignId`, etc.
|
||
- `call.direction`, `call.status`, `call.callbackStatus`, etc.
|
||
- `agent.status`, `agent.skills`, etc.
|
||
|
||
**Computed facts** — derived values that don't exist as entity fields:
|
||
- `lead.ageMinutes` — computed from `createdAt`
|
||
- `call.slaElapsedPercent` — computed from `createdAt` + task type SLA
|
||
- `call.slaBreached` — computed from slaElapsedPercent > 100
|
||
- `call.taskType` — inferred from call data (missed_call, follow_up, campaign_lead, etc.)
|
||
|
||
### Phase 2: Metadata-Driven Discovery
|
||
- Query platform metadata API to discover entities and fields dynamically
|
||
- Each field's type (NUMBER, TEXT, SELECT, BOOLEAN) drives:
|
||
- Available operators in the condition builder UI
|
||
- Input type (slider, dropdown with enum values, text, toggle)
|
||
- Computed facts remain registered in code alongside metadata-driven facts
|
||
|
||
---
|
||
|
||
## Rule Schema
|
||
|
||
```typescript
|
||
type RuleType = 'priority' | 'automation';
|
||
|
||
type Rule = {
|
||
id: string; // UUID
|
||
ruleType: RuleType; // Priority or Automation
|
||
name: string; // Human-readable
|
||
description?: string; // BA-friendly explanation
|
||
enabled: boolean; // Toggle on/off without deleting
|
||
priority: number; // Evaluation order (lower = first)
|
||
|
||
trigger: RuleTrigger; // When to evaluate
|
||
conditions: RuleConditionGroup; // What to check
|
||
action: RuleAction; // What to do
|
||
|
||
// Automation rules only
|
||
status?: 'draft' | 'published'; // Draft/publish workflow
|
||
|
||
metadata: {
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
createdBy: string;
|
||
category: RuleCategory;
|
||
tags?: string[];
|
||
};
|
||
};
|
||
|
||
type RuleTrigger =
|
||
| { type: 'on_request'; request: 'worklist' | 'assignment' }
|
||
| { type: 'on_event'; event: string }
|
||
| { type: 'on_schedule'; interval: string }
|
||
| { type: 'always' };
|
||
|
||
type RuleCategory =
|
||
| 'priority' // Worklist scoring (Priority Rules)
|
||
| 'assignment' // Lead/call routing to agent (Automation)
|
||
| 'escalation' // SLA breach handling (Automation)
|
||
| 'lifecycle' // Lead status transitions (Automation)
|
||
| 'qualification'; // Lead quality scoring (Automation)
|
||
|
||
type RuleConditionGroup = {
|
||
all?: (RuleCondition | RuleConditionGroup)[];
|
||
any?: (RuleCondition | RuleConditionGroup)[];
|
||
};
|
||
|
||
type RuleCondition = {
|
||
fact: string; // Fact name
|
||
operator: RuleOperator;
|
||
value: any;
|
||
path?: string; // JSON path for nested facts
|
||
};
|
||
|
||
type RuleOperator =
|
||
| 'equal' | 'notEqual'
|
||
| 'greaterThan' | 'greaterThanInclusive'
|
||
| 'lessThan' | 'lessThanInclusive'
|
||
| 'in' | 'notIn'
|
||
| 'contains' | 'doesNotContain'
|
||
| 'exists' | 'doesNotExist';
|
||
|
||
type RuleAction = {
|
||
type: RuleActionType;
|
||
params: ScoreActionParams | AssignActionParams | EscalateActionParams | UpdateActionParams;
|
||
};
|
||
|
||
type RuleActionType = 'score' | 'assign' | 'escalate' | 'update' | 'notify';
|
||
|
||
// Score action params (Priority Rules)
|
||
type ScoreActionParams = {
|
||
weight: number; // 0-10 base weight
|
||
slaMultiplier?: boolean; // Apply SLA urgency curve
|
||
campaignMultiplier?: boolean; // Apply campaign weight
|
||
};
|
||
|
||
// Assign action params (Automation Rules — stub)
|
||
type AssignActionParams = {
|
||
agentId?: string;
|
||
agentPool?: string[];
|
||
strategy: 'specific' | 'round-robin' | 'least-loaded' | 'skill-based';
|
||
};
|
||
|
||
// Escalate action params (Automation Rules — stub)
|
||
type EscalateActionParams = {
|
||
channel: 'toast' | 'notification' | 'sms' | 'email';
|
||
recipients: 'supervisor' | 'agent' | string[];
|
||
message: string;
|
||
severity: 'warning' | 'critical';
|
||
};
|
||
|
||
// Update action params (Automation Rules — stub)
|
||
type UpdateActionParams = {
|
||
entity: string;
|
||
field: string;
|
||
value: any;
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Priority Rules — Scoring System
|
||
|
||
### Formula
|
||
```
|
||
finalScore = baseScore × slaMultiplier × campaignMultiplier
|
||
```
|
||
|
||
### Base Score
|
||
Determined by the rule's `weight` param (0-10). Multiple rules can fire for the same item — scores are **summed**.
|
||
|
||
### SLA Multiplier (time-sensitive, computed at request time)
|
||
```
|
||
if slaElapsed <= 100%: multiplier = (slaElapsed / 100) ^ 1.6
|
||
if slaElapsed > 100%: multiplier = 1.0 + (excess × 0.05)
|
||
```
|
||
Non-linear curve — urgency accelerates as deadline approaches. Continues increasing past breach.
|
||
|
||
### Campaign Multiplier
|
||
```
|
||
campaignWeight (0-10) / 10 × sourceWeight (0-10) / 10
|
||
```
|
||
IVF(9) × WhatsApp(9) = 0.81. Health(7) × Instagram(5) = 0.35.
|
||
|
||
### Priority Config (supervisor-editable)
|
||
```typescript
|
||
type PriorityConfig = {
|
||
taskWeights: Record<string, { weight: number; slaMinutes: number }>;
|
||
campaignWeights: Record<string, number>; // campaignId → 0-10
|
||
sourceWeights: Record<string, number>; // leadSource → 0-10
|
||
};
|
||
|
||
// Default config (from hospital starter template)
|
||
const DEFAULT_PRIORITY_CONFIG = {
|
||
taskWeights: {
|
||
missed_call: { weight: 9, slaMinutes: 720 }, // 12 hours
|
||
follow_up: { weight: 8, slaMinutes: 1440 }, // 1 day
|
||
campaign_lead: { weight: 7, slaMinutes: 2880 }, // 2 days
|
||
attempt_2: { weight: 6, slaMinutes: 1440 },
|
||
attempt_3: { weight: 4, slaMinutes: 2880 },
|
||
},
|
||
campaignWeights: {}, // Empty = no campaign multiplier
|
||
sourceWeights: {
|
||
WHATSAPP: 9, PHONE: 8, FACEBOOK_AD: 7, GOOGLE_AD: 7,
|
||
INSTAGRAM: 5, WEBSITE: 7, REFERRAL: 6, WALK_IN: 5, OTHER: 5,
|
||
},
|
||
};
|
||
```
|
||
|
||
This config is what the **Priority Rules UI** edits via sliders. Under the hood, each entry generates a json-rules-engine rule.
|
||
|
||
---
|
||
|
||
## Priority Rules UI (Supervisor Settings)
|
||
|
||
### Layout
|
||
Settings page → "Priority" tab with three sections:
|
||
|
||
**Section 1: Task Type Weights**
|
||
| Task Type | Weight (slider 0-10) | SLA (input) |
|
||
|---|---|---|
|
||
| Missed Calls | ████████░░ 9 | 12h |
|
||
| Follow-ups | ███████░░░ 8 | 1d |
|
||
| Campaign Leads | ██████░░░░ 7 | 2d |
|
||
| 2nd Attempt | █████░░░░░ 6 | 1d |
|
||
| 3rd Attempt | ███░░░░░░░ 4 | 2d |
|
||
|
||
**Section 2: Campaign Weights**
|
||
Shows existing campaigns with weight sliders. Default 5.
|
||
| Campaign | Weight |
|
||
|---|---|
|
||
| IVF Awareness | ████████░░ 9 |
|
||
| Health Checkup | ██████░░░░ 7 |
|
||
| Cancer Screening | ███████░░░ 8 |
|
||
|
||
**Section 3: Source Weights**
|
||
| Source | Weight |
|
||
|---|---|
|
||
| WhatsApp | ████████░░ 9 |
|
||
| Phone | ███████░░░ 8 |
|
||
| Facebook Ad | ██████░░░░ 7 |
|
||
| ... | ... |
|
||
|
||
**Section 4: Live Preview**
|
||
Shows the current worklist re-ranked with the configured weights. As supervisor adjusts sliders, preview updates in real-time (client-side computation using the same scoring formula).
|
||
|
||
### Components
|
||
- Untitled UI Slider (if available) or custom range input
|
||
- Untitled UI Toggle for enable/disable per task type
|
||
- Untitled UI Tabs for Priority / Automations
|
||
- Score badges showing computed values in preview
|
||
|
||
---
|
||
|
||
## Storage
|
||
|
||
### Redis Keys
|
||
```
|
||
rules:config # JSON array of all Rule objects
|
||
rules:priority-config # PriorityConfig JSON (slider values)
|
||
rules:config:backup_path # Path to JSON backup file
|
||
rules:scores:{itemId} # Cached base score per worklist item
|
||
rules:scores:version # Incremented on rule change (invalidates all scores)
|
||
rules:eval:log:{ruleId} # Last evaluation result (debug)
|
||
```
|
||
|
||
### JSON File Backup
|
||
On every rule/config change:
|
||
1. Write to Redis
|
||
2. Persist to `data/rules-config.json` + `data/priority-config.json` in sidecar working directory
|
||
3. On sidecar startup: if Redis is empty, load from JSON files
|
||
|
||
---
|
||
|
||
## API Endpoints
|
||
|
||
### Priority Config (used by UI sliders)
|
||
```
|
||
GET /api/rules/priority-config # Get current priority config
|
||
PUT /api/rules/priority-config # Update priority config (slider values)
|
||
POST /api/rules/priority-config/preview # Preview scoring with modified config
|
||
```
|
||
|
||
### Rule CRUD (for automation rules)
|
||
```
|
||
GET /api/rules # List all rules
|
||
GET /api/rules/:id # Get single rule
|
||
POST /api/rules # Create rule
|
||
PUT /api/rules/:id # Update rule
|
||
DELETE /api/rules/:id # Delete rule
|
||
PATCH /api/rules/:id/toggle # Enable/disable
|
||
POST /api/rules/reorder # Change evaluation order
|
||
```
|
||
|
||
### Evaluation
|
||
```
|
||
POST /api/rules/evaluate # Evaluate rules against provided facts
|
||
```
|
||
|
||
### Templates
|
||
```
|
||
GET /api/rules/templates # List available rule templates
|
||
POST /api/rules/templates/:id/apply # Apply a template (creates rules + config)
|
||
```
|
||
|
||
---
|
||
|
||
## Worklist Integration
|
||
|
||
### Current Flow
|
||
```
|
||
GET /api/worklist → returns { missedCalls, followUps, marketingLeads } → frontend sorts by priority + createdAt
|
||
```
|
||
|
||
### New Flow
|
||
```
|
||
GET /api/worklist → fetch 3 arrays → score each item via RulesEngineService → return with scores → frontend sorts by score
|
||
```
|
||
|
||
### Response Change
|
||
Each worklist item gains:
|
||
```typescript
|
||
{
|
||
...existingFields,
|
||
score: number; // Computed priority score
|
||
scoreBreakdown: { // Explainability
|
||
baseScore: number;
|
||
slaMultiplier: number;
|
||
campaignMultiplier: number;
|
||
rulesApplied: string[]; // Rule names that fired
|
||
};
|
||
slaStatus: 'low' | 'medium' | 'high' | 'critical';
|
||
slaElapsedPercent: number;
|
||
}
|
||
```
|
||
|
||
### Frontend Changes
|
||
- Worklist sorts by `score` descending instead of hardcoded priority
|
||
- SLA status dot (green/amber/red/dark-red) replaces priority badge
|
||
- Tooltip on score shows breakdown ("IVF campaign ×0.81, Missed call weight 9, SLA 72% elapsed")
|
||
|
||
---
|
||
|
||
## Hospital Starter Template
|
||
|
||
Pre-configured priority config + automation rules for a typical hospital. Applied on first setup via `POST /api/rules/templates/hospital-starter/apply`.
|
||
|
||
Creates:
|
||
1. `PriorityConfig` with default task/campaign/source weights
|
||
2. Scoring rules in `rules:config` matching the config
|
||
3. One escalation rule stub (SLA breach → supervisor notification)
|
||
|
||
---
|
||
|
||
## Scope Boundaries
|
||
|
||
**In scope (Phase 1 — Friday):**
|
||
- `json-rules-engine` integration in sidecar
|
||
- Rule schema with `ruleType: 'priority' | 'automation'` distinction
|
||
- Curated fact providers (lead, call, agent) with computed facts
|
||
- Score action handler (full) + assign/escalate/update stubs
|
||
- Redis storage + JSON backup
|
||
- PriorityConfig CRUD + preview endpoints
|
||
- Rule CRUD API endpoints
|
||
- Worklist consumer (scoring integration)
|
||
- Hospital starter template
|
||
- **Priority Rules UI** — supervisor settings page with weight sliders, SLA config, live preview
|
||
- Frontend worklist changes (score display, SLA dots, breakdown tooltip)
|
||
|
||
**Out of scope (Phase 2+):**
|
||
- Automation Rules UI (condition builder with entity/field selectors)
|
||
- Metadata-driven fact discovery from platform API
|
||
- Assignment/escalation/update action handlers (stubs in Phase 1)
|
||
- Event-driven rule evaluation (on_event triggers)
|
||
- Scheduled rule evaluation (on_schedule triggers)
|
||
- Draft/publish workflow for automation rules
|
||
- Multi-tenant rule isolation
|