Rule-Based Decision Engine
Overview
Section titled “Overview”A Rule-Based System (RBS) is a deterministic decision-making engine that uses predefined domain knowledge encoded as “IF-THEN” rules to:
- Match incoming data against diagnosis or assessment rules
- Suggest appropriate actions or interventions based on matched rules
- Validate action safety (age, category, contraindications)
- Check inventory or resource availability
- Recommend alternatives when primary options are unavailable
Key Characteristics
Section titled “Key Characteristics”- Deterministic: Same input always produces same output
- Transparent: Domain experts can understand and verify each decision
- Auditable: Easier to trace than “black-box” AI models
- Expert-Encoded: Rules created and maintained by domain professionals
- Safe Fallback: Always requires human verification before execution
Architecture Overview
Section titled “Architecture Overview”graph TB
A[Input Data] --> B[Symptom / Condition Input<br/>Vital Parameters]
B --> C{Rule Engine}
C --> D[Assessment Rules]
D --> E[Primary Assessment]
E --> F[Action / Intervention Rules]
F --> G[Suggested Actions]
G --> H[Safety Validator]
H --> I{Check Category}
H --> J{Check Profile}
H --> K{Check Contraindications}
I --> L{All Safe?}
J --> L
K --> L
L -->|Yes| M[Inventory / Availability Check]
L -->|No| N[Safety Alert — Suggest Alternatives]
M --> O{Resource Available?}
O -->|Yes| P[Final Recommendation]
O -->|No| Q[Find Alternatives in Same Class]
Q --> M
N --> R[Manual Review Required]
P --> S[Display to Operator]
style E fill:#e1f5ff
style G fill:#e8f5e9
style N fill:#ffebee
style P fill:#c8e6c9
style Q fill:#fff9c4
System Components
Section titled “System Components”1. Knowledge Base (Rules Database)
Section titled “1. Knowledge Base (Rules Database)”Domain knowledge encoded as structured rules stored in PostgreSQL:
-- Assessment Rules TableCREATE TABLE assessment_rules ( id UUID PRIMARY KEY, rule_code VARCHAR(50) UNIQUE NOT NULL, assessment_code VARCHAR(10) NOT NULL, assessment_name_en VARCHAR(255) NOT NULL, priority INT DEFAULT 0, conditions JSONB NOT NULL, -- Rule conditions confidence_level DECIMAL(3,2), -- 0.00-1.00 created_by UUID REFERENCES lookup_users(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), is_active BOOLEAN DEFAULT TRUE);
-- Action / Intervention Rules TableCREATE TABLE action_rules ( id UUID PRIMARY KEY, rule_code VARCHAR(50) UNIQUE NOT NULL, assessment_code VARCHAR(10) NOT NULL, action_generic_name VARCHAR(255) NOT NULL, action_display_name VARCHAR(255), parameters VARCHAR(100) NOT NULL, -- dosage / configuration frequency VARCHAR(100) NOT NULL, duration VARCHAR(100) NOT NULL, route VARCHAR(50) NOT NULL, -- delivery method priority INT DEFAULT 0, contraindications JSONB, -- Category, profile, conditions alternatives JSONB, -- Alternative actions created_at TIMESTAMPTZ DEFAULT NOW(), is_active BOOLEAN DEFAULT TRUE);
-- Resource Inventory TableCREATE TABLE resource_inventory ( id UUID PRIMARY KEY, generic_name VARCHAR(255) NOT NULL, display_name VARCHAR(255), item_code VARCHAR(50) UNIQUE NOT NULL, quantity_available INT NOT NULL, unit VARCHAR(50) NOT NULL, expiry_date DATE, reorder_level INT DEFAULT 0, is_available BOOLEAN DEFAULT TRUE);
-- Resource Contraindications TableCREATE TABLE resource_contraindications ( id UUID PRIMARY KEY, resource_id UUID REFERENCES resources(id), contraindication_type VARCHAR(50) NOT NULL, -- CATEGORY, PROFILE, OTHER contraindication_name VARCHAR(255) NOT NULL, reaction VARCHAR(255), severity VARCHAR(50), -- MILD, MODERATE, SEVERE, CRITICAL recorded_date DATE NOT NULL, recorded_by UUID REFERENCES lookup_users(id));2. Rule Engine Service
Section titled “2. Rule Engine Service”NestJS service that processes rules and makes decisions:
import { Injectable } from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';
import { BadRequestException, NotFoundException } from '@lib/common';import { AppDatabases } from '@lib/common/enum/app-databases.enum';
import { AssessmentRule } from '../entities/assessment-rule.entity';import { ActionRule } from '../entities/action-rule.entity';import { ResourceInventory } from '../entities/resource-inventory.entity';
interface ResourceProfile { age: number; category: string; // e.g., 'A', 'B', 'C' or domain-specific category conditions: string[]; // List of presenting conditions or symptoms parameters: { temperature?: number; pressureSystolic?: number; pressureDiastolic?: number; heartRate?: number; respiratoryRate?: number; oxygenSaturation?: number; }; contraindications: string[]; // Known contraindications for this resource}
interface AssessmentResult { assessmentCode: string; assessmentNameEn: string; confidenceLevel: number; matchedRules: string[]; suggestedActions: ActionRecommendation[];}
interface ActionRecommendation { genericName: string; displayName?: string; parameters: string; frequency: string; duration: string; route: string; isAvailableInInventory: boolean; quantityAvailable?: number; safetyChecks: SafetyCheck; alternatives?: AlternativeAction[];}
interface SafetyCheck { isSafe: boolean; warnings: string[]; contraindications: string[];}
interface AlternativeAction { genericName: string; displayName?: string; reason: string; isAvailableInInventory: boolean;}
@Injectable()export class RuleEngineService { constructor( @InjectRepository(AssessmentRule, AppDatabases.APP_CORE) private assessmentRuleRepo: Repository<AssessmentRule>, @InjectRepository(ActionRule, AppDatabases.APP_CORE) private actionRuleRepo: Repository<ActionRule>, @InjectRepository(ResourceInventory, AppDatabases.APP_CORE) private resourceInventoryRepo: Repository<ResourceInventory>, ) {}
/** * Main method: Assess and suggest actions */ async assessAndSuggestActions( profile: ResourceProfile, ): Promise<AssessmentResult[]> { // Step 1: Match assessment rules const assessments = await this.matchAssessmentRules(profile);
if (assessments.length === 0) { throw new NotFoundException( 'No matching assessment found. Manual review required.', ); }
// Step 2: For each assessment, suggest actions for (const assessment of assessments) { assessment.suggestedActions = await this.suggestActions( assessment.assessmentCode, profile, ); }
// Step 3: Sort by confidence level (descending) return assessments.sort((a, b) => b.confidenceLevel - a.confidenceLevel); }
/** * Step 1: Match profile data against assessment rules */ private async matchAssessmentRules( profile: ResourceProfile, ): Promise<AssessmentResult[]> { const allRules = await this.assessmentRuleRepo.find({ where: { is_active: true }, order: { priority: 'DESC' }, });
const matched: AssessmentResult[] = [];
for (const rule of allRules) { const conditions = rule.conditions as Record<string, unknown>; const matchResult = this.evaluateConditions(conditions, profile);
if (matchResult.isMatch) { matched.push({ assessmentCode: rule.assessment_code, assessmentNameEn: rule.assessment_name_en, confidenceLevel: rule.confidence_level, matchedRules: [rule.rule_code], suggestedActions: [], // Filled later }); } }
return matched; }
/** * Evaluate rule conditions against profile data */ private evaluateConditions( conditions: Record<string, unknown>, profile: ResourceProfile, ): { isMatch: boolean; matchedConditions: string[] } { const matchedConditions: string[] = []; let totalConditions = 0; let matchedCount = 0;
// Check presenting conditions const requiredConditions = conditions.conditions as string[] | undefined; if (requiredConditions && requiredConditions.length > 0) { for (const required of requiredConditions) { totalConditions++; if (profile.conditions.some((c) => c.toLowerCase().includes(required.toLowerCase()))) { matchedCount++; matchedConditions.push(`Condition: ${required}`); } } }
// Check parameters const params = conditions.parameters as Record<string, unknown> | undefined; if (params !== undefined) { if (params.temperature !== undefined) { totalConditions++; const temp = profile.parameters.temperature; if (temp !== undefined && this.checkRange(temp, params.temperature as { min?: number; max?: number })) { matchedCount++; matchedConditions.push(`Temperature: ${temp}`); } }
if (params.pressure !== undefined) { totalConditions++; const systolic = profile.parameters.pressureSystolic; const diastolic = profile.parameters.pressureDiastolic; const pressureRange = params.pressure as { systolic?: { min?: number; max?: number }; diastolic?: { min?: number; max?: number } }; if ( systolic !== undefined && diastolic !== undefined && pressureRange.systolic !== undefined && this.checkRange(systolic, pressureRange.systolic) && pressureRange.diastolic !== undefined && this.checkRange(diastolic, pressureRange.diastolic) ) { matchedCount++; matchedConditions.push(`Pressure: ${systolic}/${diastolic}`); } }
if (params.heartRate !== undefined) { totalConditions++; const hr = profile.parameters.heartRate; if (hr !== undefined && this.checkRange(hr, params.heartRate as { min?: number; max?: number })) { matchedCount++; matchedConditions.push(`Heart Rate: ${hr}`); } } }
// Check age range const ageRange = conditions.ageRange as { min?: number; max?: number } | undefined; if (ageRange !== undefined) { totalConditions++; const age = profile.age; const minAge = ageRange.min ?? 0; const maxAge = ageRange.max ?? Infinity; if (age >= minAge && age <= maxAge) { matchedCount++; matchedConditions.push(`Age: ${age}`); } }
// Check category if (conditions.category !== undefined) { totalConditions++; if (profile.category === conditions.category) { matchedCount++; matchedConditions.push(`Category: ${profile.category}`); } }
// Require at least 70% match const matchPercentage = totalConditions > 0 ? matchedCount / totalConditions : 0; return { isMatch: matchPercentage >= 0.7, matchedConditions }; }
private checkRange(value: number, range: { min?: number; max?: number }): boolean { if (range.min !== undefined && value < range.min) return false; if (range.max !== undefined && value > range.max) return false; return true; }
/** * Step 2: Suggest actions for an assessment */ private async suggestActions( assessmentCode: string, profile: ResourceProfile, ): Promise<ActionRecommendation[]> { const actionRules = await this.actionRuleRepo.find({ where: { assessment_code: assessmentCode, is_active: true }, order: { priority: 'DESC' }, });
const recommendations: ActionRecommendation[] = [];
for (const rule of actionRules) { // Step 2.1: Safety checks const safetyCheck = this.performSafetyChecks(rule, profile);
// Step 2.2: Inventory / availability check const inventoryCheck = await this.checkInventory(rule.action_generic_name);
// Step 2.3: Find alternatives if needed let alternatives: AlternativeAction[] = []; if (!safetyCheck.isSafe || !inventoryCheck.isAvailable) { alternatives = await this.findAlternatives(rule, profile); }
recommendations.push({ genericName: rule.action_generic_name, displayName: rule.action_display_name, parameters: rule.parameters, frequency: rule.frequency, duration: rule.duration, route: rule.route, isAvailableInInventory: inventoryCheck.isAvailable, quantityAvailable: inventoryCheck.quantity, safetyChecks: safetyCheck, alternatives: alternatives.length > 0 ? alternatives : undefined, }); }
return recommendations; }
/** * Step 3: Perform safety checks (age, category, contraindications) */ private performSafetyChecks( rule: ActionRule, profile: ResourceProfile, ): SafetyCheck { const warnings: string[] = []; const contraindications: string[] = []; let isSafe = true;
const ci = rule.contraindications as Record<string, unknown> | null; if (ci === null) { return { isSafe: true, warnings: [], contraindications: [] }; }
// Check age restrictions const ageRestrictions = ci.ageRestrictions as { minAge?: number; maxAge?: number } | undefined; if (ageRestrictions !== undefined) { if (ageRestrictions.minAge !== undefined && profile.age < ageRestrictions.minAge) { isSafe = false; contraindications.push(`Age (${profile.age}) is below minimum (${ageRestrictions.minAge})`); } if (ageRestrictions.maxAge !== undefined && profile.age > ageRestrictions.maxAge) { isSafe = false; contraindications.push(`Age (${profile.age}) is above maximum (${ageRestrictions.maxAge})`); } }
// Check category restrictions const categoryRestrictions = ci.categoryRestrictions as string[] | undefined; if (categoryRestrictions !== undefined && categoryRestrictions.length > 0) { if (!categoryRestrictions.includes(profile.category)) { isSafe = false; contraindications.push(`Action not suitable for category: ${profile.category}`); } }
// Check known contraindications if (profile.contraindications.length > 0) { const actionName = rule.action_generic_name.toLowerCase(); for (const contra of profile.contraindications) { if (actionName.includes(contra.toLowerCase())) { isSafe = false; contraindications.push(`Known contraindication: ${contra}`); } } }
// Attach special warnings const ciWarnings = ci.warnings as string[] | undefined; if (ciWarnings !== undefined) { warnings.push(...ciWarnings); }
return { isSafe, warnings, contraindications }; }
/** * Step 4: Check inventory / resource availability */ private async checkInventory( genericName: string, ): Promise<{ isAvailable: boolean; quantity?: number }> { const inventory = await this.resourceInventoryRepo.findOne({ where: { generic_name: genericName, is_available: true }, });
if (inventory === null) { return { isAvailable: false }; }
const isAvailable = inventory.quantity_available > inventory.reorder_level; return { isAvailable, quantity: inventory.quantity_available }; }
/** * Step 5: Find alternative actions */ private async findAlternatives( originalRule: ActionRule, profile: ResourceProfile, ): Promise<AlternativeAction[]> { const alternativesData = originalRule.alternatives as Array<{ genericName: string; displayName?: string; reason?: string }> | null; if (alternativesData === null || alternativesData.length === 0) { return []; }
const alternatives: AlternativeAction[] = [];
for (const alt of alternativesData) { const altRule = await this.actionRuleRepo.findOne({ where: { action_generic_name: alt.genericName, is_active: true }, }); if (altRule === null) continue;
const safetyCheck = this.performSafetyChecks(altRule, profile); if (!safetyCheck.isSafe) continue;
const inventoryCheck = await this.checkInventory(alt.genericName);
alternatives.push({ genericName: alt.genericName, displayName: alt.displayName, reason: alt.reason ?? 'Alternative in same action class', isAvailableInInventory: inventoryCheck.isAvailable, }); }
return alternatives; }}Use Case 1: Hypertensive Crisis
Section titled “Use Case 1: Hypertensive Crisis”Scenario: 55-year-old male presenting with severe hypertension.
Input Profile
Section titled “Input Profile”{ "age": 55, "category": "M", "conditions": ["severe headache", "blurred vision", "chest discomfort"], "parameters": { "temperature": 37.2, "pressureSystolic": 195, "pressureDiastolic": 115, "heartRate": 92, "respiratoryRate": 18, "oxygenSaturation": 97 }, "contraindications": []}Assessment Rule Seed
Section titled “Assessment Rule Seed”INSERT INTO assessment_rules ( rule_code, assessment_code, assessment_name_en, priority, conditions, confidence_level) VALUES ( 'HYPERTENSIVE_CRISIS_01', 'I16.0', 'Hypertensive Crisis', 10, '{ "conditions": ["headache", "blurred vision"], "parameters": { "pressure": { "systolic": { "min": 180 }, "diastolic": { "min": 110 } } }, "ageRange": { "min": 18, "max": 120 } }'::jsonb, 0.95);Action Rules Seed
Section titled “Action Rules Seed”Primary Action:
INSERT INTO action_rules ( rule_code, assessment_code, action_generic_name, action_display_name, parameters, frequency, duration, route, priority, contraindications, alternatives) VALUES ( 'ACT_HYPERTENSIVE_CRISIS_01', 'I16.0', 'Amlodipine', 'Norvasc', '10 mg', 'Once daily', '30 days', 'Oral', 10, '{ "ageRestrictions": { "minAge": 18 }, "categoryRestrictions": [], "warnings": [ "Monitor pressure daily", "Report any swelling in feet or ankles" ] }'::jsonb, '[ { "genericName": "Losartan", "displayName": "Cozaar", "reason": "Alternative ARB if calcium channel blocker not tolerated" }, { "genericName": "Enalapril", "displayName": "Vasotec", "reason": "Alternative ACE inhibitor for pressure control" } ]'::jsonb);Inventory Data
Section titled “Inventory Data”INSERT INTO resource_inventory ( generic_name, display_name, item_code, quantity_available, unit, expiry_date, reorder_level, is_available) VALUES('Amlodipine', 'Norvasc', 'DRG-AMLOD-10MG', 150, 'tablets', '2026-12-31', 50, true),('Losartan', 'Cozaar', 'DRG-LOSAR-50MG', 200, 'tablets', '2026-06-30', 50, true),('Enalapril', 'Vasotec', 'DRG-ENALA-5MG', 5, 'tablets', '2026-03-31', 50, false); -- Low stock!API Request
Section titled “API Request”POST /data-owner-bc/v1/assessment/suggestContent-Type: application/jsonAuthorization: Bearer {JWT_TOKEN}
{ "resourceId": "uuid-resource-123", "age": 55, "category": "M", "conditions": ["severe headache", "blurred vision", "chest discomfort"], "parameters": { "temperature": 37.2, "pressureSystolic": 195, "pressureDiastolic": 115, "heartRate": 92, "respiratoryRate": 18, "oxygenSaturation": 97 }, "contraindications": []}API Response
Section titled “API Response”{ "status": { "code": 200000, "message": "Request Succeeded" }, "data": { "type": "assessment-suggestions", "attributes": { "assessments": [ { "assessment_code": "I16.0", "assessment_name_en": "Hypertensive Crisis", "confidence_level": 0.95, "matched_rules": ["HYPERTENSIVE_CRISIS_01"], "suggested_actions": [ { "generic_name": "Amlodipine", "display_name": "Norvasc", "parameters": "10 mg", "frequency": "Once daily", "duration": "30 days", "route": "Oral", "is_available_in_inventory": true, "quantity_available": 150, "safety_checks": { "is_safe": true, "warnings": ["Monitor pressure daily"], "contraindications": [] } }, { "generic_name": "Losartan", "display_name": "Cozaar", "parameters": "50 mg", "frequency": "Once daily", "duration": "30 days", "route": "Oral", "is_available_in_inventory": true, "quantity_available": 200, "safety_checks": { "is_safe": true, "warnings": ["Check potassium levels regularly"], "contraindications": [] } } ] } ] } }}Key Points:
- ✅ Primary assessment matched with 95% confidence
- ✅ Amlodipine available in inventory (150 units)
- ✅ Losartan available as alternative (200 units)
- ✅ No contraindications for this profile
- ⚠️ Enalapril not suggested (low stock, below reorder level)
Use Case 2: Age-Based Contraindication
Section titled “Use Case 2: Age-Based Contraindication”Scenario: 8-year-old in category F presenting with high fever. Aspirin is contraindicated due to Reye’s syndrome risk in children.
Input Profile
Section titled “Input Profile”{ "age": 8, "category": "F", "conditions": ["high fever", "body aches", "headache"], "parameters": { "temperature": 39.5, "pressureSystolic": 95, "pressureDiastolic": 60, "heartRate": 110, "respiratoryRate": 22, "oxygenSaturation": 98 }, "contraindications": []}Assessment Rule
Section titled “Assessment Rule”INSERT INTO assessment_rules ( rule_code, assessment_code, assessment_name_en, priority, conditions, confidence_level) VALUES ( 'FEVER_PEDIATRIC_01', 'R50.9', 'Fever of Unknown Origin', 8, '{ "conditions": ["fever", "body aches"], "parameters": { "temperature": { "min": 38.5 } }, "ageRange": { "min": 0, "max": 18 } }'::jsonb, 0.85);Action Rules
Section titled “Action Rules”Contraindicated action (Aspirin — for illustration):
INSERT INTO action_rules ( rule_code, assessment_code, action_generic_name, action_display_name, parameters, frequency, duration, route, priority, contraindications, alternatives) VALUES ( 'ACT_FEVER_ASPIRIN', 'R50.9', 'Aspirin', 'Bayer', '500 mg', 'Every 4-6 hours', '3 days', 'Oral', 5, '{ "ageRestrictions": { "minAge": 18, "reason": "Risk of Reye syndrome in children" }, "categoryRestrictions": [], "warnings": ["Do NOT use in resources under 18 years"] }'::jsonb, '[ { "genericName": "Paracetamol", "displayName": "Tylenol", "reason": "Safe antipyretic for all ages" }, { "genericName": "Ibuprofen", "displayName": "Advil", "reason": "Alternative safe for ages over 6 months" } ]'::jsonb);Safe action (Paracetamol):
INSERT INTO action_rules ( rule_code, assessment_code, action_generic_name, action_display_name, parameters, frequency, duration, route, priority, contraindications, alternatives) VALUES ( 'ACT_FEVER_PARACETAMOL', 'R50.9', 'Paracetamol', 'Tylenol', '250 mg', 'Every 4-6 hours', '3 days', 'Oral', 10, '{ "ageRestrictions": { "minAge": 0 }, "categoryRestrictions": [], "warnings": ["Do not exceed recommended daily dose", "Monitor if prolonged use"] }'::jsonb, '[ { "genericName": "Ibuprofen", "displayName": "Advil", "reason": "Alternative for fever reduction" } ]'::jsonb);API Response
Section titled “API Response”The rule engine detects the age contraindication for Aspirin and surfaces it, while recommending Paracetamol as the safe primary action:
{ "data": { "type": "assessment-suggestions", "attributes": { "assessments": [{ "assessment_code": "R50.9", "assessment_name_en": "Fever of Unknown Origin", "confidence_level": 0.85, "suggested_actions": [ { "generic_name": "Paracetamol", "is_available_in_inventory": true, "quantity_available": 500, "safety_checks": { "is_safe": true, "warnings": ["Do not exceed recommended daily dose"], "contraindications": [] } }, { "generic_name": "Aspirin", "is_available_in_inventory": true, "quantity_available": 300, "safety_checks": { "is_safe": false, "warnings": ["Do NOT use in resources under 18 years"], "contraindications": [ "Age (8) is below minimum (18)", "Risk of Reye syndrome in children" ] }, "alternatives": [ { "generic_name": "Paracetamol", "reason": "Safe antipyretic for all ages", "is_available_in_inventory": true }, { "generic_name": "Ibuprofen", "reason": "Alternative safe for ages over 6 months", "is_available_in_inventory": true } ] } ] }] } }}Key Points:
- ✅ Paracetamol recommended as safe primary action
- ❌ Aspirin flagged with
is_safe: falseand explicit contraindication reason - ✅ Alternatives surfaced automatically (Paracetamol, Ibuprofen)
- ✅ Human operator sees full transparency — no black-box AI decision
Design Principles
Section titled “Design Principles”| Principle | Implementation |
|---|---|
| Transparency | Every recommendation traces back to a named rule_code |
| Safety First | Contraindications block recommendations before inventory is checked |
| Graceful Alternatives | When primary fails safety or stock check, alternatives are surfaced automatically |
| Human-in-the-Loop | The system suggests; humans approve before execution |
| Auditability | rule_code, confidence_level, matched_rules all stored on every result |
Related Documentation
Section titled “Related Documentation”- AI Integration (Ollama + RAG) — Full RAG pipeline for context-aware explanations
- Hybrid AI Architecture (RBS + RAG) — Intelligent routing between deterministic RBS and AI-powered RAG