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,123 @@
// src/rules-engine/rules-engine.controller.ts
import { Controller, Get, Post, Put, Delete, Patch, Param, Body, HttpException, Logger } from '@nestjs/common';
import { RulesStorageService } from './rules-storage.service';
import { RulesEngineService } from './rules-engine.service';
import type { Rule, PriorityConfig } from './types/rule.types';
import { readFileSync } from 'fs';
import { join } from 'path';
@Controller('api/rules')
export class RulesEngineController {
private readonly logger = new Logger(RulesEngineController.name);
constructor(
private readonly storage: RulesStorageService,
private readonly engine: RulesEngineService,
) {}
// --- Priority Config (slider UI) ---
@Get('priority-config')
async getPriorityConfig() {
return this.storage.getPriorityConfig();
}
@Put('priority-config')
async updatePriorityConfig(@Body() body: PriorityConfig) {
return this.storage.updatePriorityConfig(body);
}
// --- Rule CRUD ---
@Get()
async listRules() {
return this.storage.getAll();
}
@Get(':id')
async getRule(@Param('id') id: string) {
const rule = await this.storage.getById(id);
if (!rule) throw new HttpException('Rule not found', 404);
return rule;
}
@Post()
async createRule(@Body() body: any) {
if (!body.name || !body.trigger || !body.conditions || !body.action) {
throw new HttpException('name, trigger, conditions, and action are required', 400);
}
return this.storage.create({
...body,
ruleType: body.ruleType ?? 'priority',
enabled: body.enabled ?? true,
priority: body.priority ?? 99,
});
}
@Put(':id')
async updateRule(@Param('id') id: string, @Body() body: Partial<Rule>) {
const updated = await this.storage.update(id, body);
if (!updated) throw new HttpException('Rule not found', 404);
return updated;
}
@Delete(':id')
async deleteRule(@Param('id') id: string) {
const deleted = await this.storage.delete(id);
if (!deleted) throw new HttpException('Rule not found', 404);
return { status: 'ok' };
}
@Patch(':id/toggle')
async toggleRule(@Param('id') id: string) {
const toggled = await this.storage.toggle(id);
if (!toggled) throw new HttpException('Rule not found', 404);
return toggled;
}
@Post('reorder')
async reorderRules(@Body() body: { ids: string[] }) {
if (!body.ids?.length) throw new HttpException('ids array required', 400);
return this.storage.reorder(body.ids);
}
// --- Evaluation ---
@Post('evaluate')
async evaluate(@Body() body: { trigger: string; triggerValue: string; facts: Record<string, any> }) {
return this.engine.evaluate(body.trigger, body.triggerValue, body.facts);
}
// --- Templates ---
@Get('templates/list')
async listTemplates() {
return [{ id: 'hospital-starter', name: 'Hospital Starter Pack', description: 'Default rules for a hospital call center', ruleCount: 7 }];
}
@Post('templates/:id/apply')
async applyTemplate(@Param('id') id: string) {
if (id !== 'hospital-starter') throw new HttpException('Template not found', 404);
let template: any;
try {
template = JSON.parse(readFileSync(join(__dirname, 'templates', 'hospital-starter.json'), 'utf8'));
} catch {
throw new HttpException('Failed to load template', 500);
}
// Apply priority config
await this.storage.updatePriorityConfig(template.priorityConfig);
// Create rules
const created: Rule[] = [];
for (const rule of template.rules) {
const newRule = await this.storage.create(rule);
created.push(newRule);
}
this.logger.log(`Applied hospital-starter template: ${created.length} rules + priority config`);
return { status: 'ok', rulesCreated: created.length, rules: created };
}
}