An extension to Simple Rule Engine that illustrates how rules can be declaratively specified (json, yaml, custom grammar etc.), stored, and later de-serialized into simple-rule-engine constructs and executed with data.
Bureau Score | Business Ownership | Decision |
---|---|---|
between 650 and 800 | in [Owned by Self, Owned by Family] | GO |
{
"RuleDecision": {
"RuleRows": [
{
"WhenAll": [
{
"NumericToken": "cibil_score",
"Between": {
"floor": 650,
"ceiling": 800
}
},
{
"StringToken": "business_ownership",
"In": [
"Owned by Self",
"Owned by Family"
]
}
],
"Consequent": "GO"
}
]
}
}
from unittest import TestCase
from services.adapter.simple_rule_engine_dict_adapter import SimpleRuleEngineDictAdapter
from services.util.json_file_util import JsonFileUtil
class TestSimpleRuleEngineAdapter(TestCase):
def test_rule_simple_decision(self):
json_file_util = JsonFileUtil(file_name_with_path="./examples/simple_decision.json")
decision_rule_dict = json_file_util.read_file()
rule_engine_adapter = SimpleRuleEngineDictAdapter(rule_dict=decision_rule_dict)
decision_rule = rule_engine_adapter.get_rule()
assert type(decision_rule).__name__ == "RuleDecision"
fact = dict(cibil_score=700, business_ownership="Owned by Self")
assert decision_rule.execute(token_dict=fact) == "GO"
- If age >= 35 and pet in dog, score is 10, with a weight of 0.5
- If domicile is in KA, score is 5, with a weight of 0.5
{
"RuleScore": {
"RuleSets": [
{
"RuleRows": [
{
"WhenAll": [
{
"NumericToken": "age",
"Gte": 35
},
{
"StringToken": "pet",
"In": [
"dog"
]
}
],
"Consequent": 10
}
],
"Weight": 0.5
},
{
"RuleRows": [
{
"WhenAll": [
{
"StringToken": "domicile",
"In": [
"KA"
]
}
],
"Consequent": 5
}
],
"Weight": 0.5
}
]
}
}
from unittest import TestCase
from services.adapter.simple_rule_engine_dict_adapter import SimpleRuleEngineDictAdapter
from services.util.json_file_util import JsonFileUtil
class TestSimpleRuleEngineAdapter(TestCase):
def test_rule_simple_score(self):
json_file_util = JsonFileUtil(file_name_with_path="./examples/simple_score.json")
score_rule_dict = json_file_util.read_file()
rule_engine_adapter = SimpleRuleEngineDictAdapter(rule_dict=score_rule_dict)
score_rule = rule_engine_adapter.get_rule()
assert type(score_rule).__name__ == "RuleScore"
fact = dict(age=40, pet="dog", domicile="TN")
assert score_rule.execute(token_dict=fact) == 5.0
fact = dict(age=40, pet="dog", domicile="KA")
assert score_rule.execute(token_dict=fact) == 7.5
Here's an illustration of a rule that's based on a custom grammar written in EBNF and parsed by Lark.
my_rule {
when {
cibil_score between 650 and 750 and
age > 35 and
house_ownership in (owned, rented) and
(
total_overdue_amount == 0 or
number_of_overdue_loans < 2 or
(
number_of_overdue_loans >= 2 and
big_shot == true
)
) and
pet == dog
}
then true
when {
cibil_score < 650
}
then false
}
start
decisionrule
my_rule
rulerow
when
condition
expression
token cibil_score
between
number 650
number 750
conditional and
expression
token age
gt
number 35
conditional and
expression
token house_ownership
in
word_list
owned
rented
conditional and
expression
expression
token total_overdue_amount
eq
number 0
conditional or
expression
token number_of_overdue_loans
lt
number 2
conditional or
expression
expression
token number_of_overdue_loans
gte
number 2
conditional and
expression
token big_shot
eq
boolean true
conditional and
expression
token pet
eq
string dog
then
decision
boolean true
rulerow
when
condition
expression
token cibil_score
lt
number 650
then
decision
boolean false
import pytest
from lark import Lark
from services.adapter.simple_rule_engine_lark_tree_adapter import SimpleRuleEngineLarkTreeAdapter
@pytest.fixture
def decision_rule_grammar():
with open("./decision_rule.lark") as rule_grammar_file:
rule_grammar = rule_grammar_file.read()
return rule_grammar
def test_rule_complex_decision(decision_rule_grammar):
parser = Lark(decision_rule_grammar)
custom_rule = """
my_rule {
when {
cibil_score between 650 and 750 and
age > 35 and
house_ownership in (owned, rented) and
(
total_overdue_amount == 0 or
number_of_overdue_loans < 2 or
(
number_of_overdue_loans >= 2 and
big_shot == true
)
) and
pet == dog
}
then true
when {
cibil_score < 650
}
then false
}
"""
tree = parser.parse(custom_rule)
print(tree.pretty())
decision_rule = SimpleRuleEngineLarkTreeAdapter(tree).get_rule()
# Evaluate the Decision Rule by passing data
facts = dict(
cibil_score=700,
age=40,
house_ownership="owned",
total_overdue_amount=0,
pet="dog"
)
assert decision_rule.execute(token_dict=facts) is True
facts = dict(
cibil_score=700,
age=40,
house_ownership="owned",
total_overdue_amount=100,
number_of_overdue_loans=1,
pet="dog"
)
assert decision_rule.execute(token_dict=facts) is True
facts = dict(
cibil_score=700,
age=40,
house_ownership="owned",
total_overdue_amount=100,
number_of_overdue_loans=2,
big_shot="true",
pet="dog"
)
assert decision_rule.execute(token_dict=facts) is True
facts = dict(
cibil_score=600,
age=40,
house_ownership="owned",
total_overdue_amount=100,
number_of_overdue_loans=2,
big_shot="false",
pet="dog"
)
assert decision_rule.execute(token_dict=facts) is False