While there are many existing rules engines in Python, such as the amazing rule-engine framework, I thought it would be an interesting exercise to utilize ChatGPT to help me write my own basic rule engine from scratch. So, without further ado - I present to you PYROSE - PYthon Rule-based Operation System Engine.
The basic structure of our rule system is as follows. At the basic foundation of engine we have Facts. Facts are simple objects that represent information we want stored in our system and can contain any kind of information that is relevant to the design of the constraints by which our rules-engine operates. Let us begin by defining a very simple structure for our Facts
from typing import Any
class Fact:
def __init__(self, **kwargs: Any):
self.__dict__.update(kwargs)
Here we define a very simple object that can be instantiated with any number of keyword arguments to define our fact. We then update our object used the self.__dict__.update
method to add attributes to our object corresponding to the keywords passed to the initializer. We can initialize a fact as follows,
person_fact = Fact(name="John Brown", age="35", occuptation="Software Developer")
Accessing a attribute is as simple using dot notation as if you were access a member or method on the object.
person_fact.age # Returns 35
Now that we have established our Fact object, we need to define a Condition by which to apply our Fact object. Our Conditions are very simple in design as well to start off with. The initializer takes two parameters, a name and an evaluation function. The evaluation function will be applied to a Fact and return a bool. It also contains a method, evaluate, which will take a Fact and return a bool, calling the objects evaluation_function on the Fact. Here is the outline of our Condition class:
from rule_engine.fact import Fact
from typing import Callable, Any, Dict, List
class Condition:
def __init__(self, name: str, evaluation_function: Callable[[Fact], bool]):
self.name = name
self.eval_func = evaluation_function
def evaluate(self, fact: Fact) -> bool:
return self.eval_func(fact)
Following a similar suit, we can define our Action class. An Action class takes in a name parameter and a Callable parameter into its initializer. The Callable corresponds to the Action's execution function, what is executed if all the Conditions of the Rule are True. It also contains an execute method which calls the execute function on the given Fact, giving a None type as a result. Here is the definition of the Action class:
from typing import Callable, Any, Dict, List
from rule_engine.fact import Fact
class Action:
def __init__(self, name: str, execution_function: Callable[[Fact], None]):
self.name = name
self.exec_func = execution_function
def execute(self, fact: Fact) -> None:
self.exec_func(fact)
So far we've defined our Fact, our Condition, and our Action. We can combine these together to form our Rule class. This will be the main driving force behind our Engine. The Rule class will initially be constructed with a single Action and Condition. Two methods, add_condition and add_action, will allow you to add additional conditions and actions to the Rule as you need.
Finally, a third method, evaluate, will take in a list of Facts. It defines a fact_generator which takes a list of conditions and a list of facts. For each fact, it maps each of the conditions' eval_func against the fact. It then reduces this list to a single boolean value, and if this value is true, we yield the fact.
We then call the fact_generator function, wrap it in a list to get the list of all True facts, and if the length is greater than 0, we iterate through the list of true facts. For each true fact, we iterate through a list of the rules actions and call the action's exec_func on the true fact.
The complete definition of the Rule class is as follows:
from rule_engine.condition import Condition
from rule_engine.action import Action
from rule_engine.fact import Fact
from typing import Any, List
from functools import reduce
class Rule:
def __init__(self, condition: Condition, action: Action):
self.conditions = [condition]
self.actions = [action]
def add_condition(self, condition: Condition) -> None:
self.conditions.append(condition)
def add_action(self, action: Action) -> None:
self.actions.append(action)
def evaluate(self, facts: List[Fact]) -> Any:
def fact_generator(conditions: List[Condition], facts: List[Fact]):
all_conditions_true = True
for fact in facts:
results = map(lambda condition: condition.eval_func(fact), conditions)
all_conditions_true = reduce(lambda x, y: x and y, results)
if all_conditions_true:
yield fact
true_facts = list(fact_generator(self.conditions, facts))
if len(true_facts) > 0:
for fact in true_facts:
for action in self.actions:
action.exec_func(fact)
You can use the rule engine as follows:
from rule_engine.fact import Fact
from rule_engine.condition import Condition
from rule_engine.action import Action
from rule_engine.rule import Rule
age_cond = Condition(name="Age>=21", evaluation_function=lambda fact: fact.age >= 21)
occupation_cond = Condition(name="Occupation==Software Developer", evaluation_function=lambda fact: fact.occupation == "Software Developer")
print_action = Action(name="Print Fact", execution_function=lambda fact: print("Name: {} Age: {} Occupation: {}".format(fact.name, fact.age, fact.occupation)))
john = Fact(age=25,name="John Brown", occupation="Software Developer")
sarah = Fact(age=35,name="Sarah Purple", occupation="Data Engineer")
barry = Fact(age=27, name="Barry White", occupation="Software Developer")
rule = Rule(condition=age_cond, action=print_action)
rule.add_condition(occupation_cond)
rule.evaluate([john, sarah, barry])
All in all, this is a very basic demo of a very simplistic Rules engine. However, I think it can definitely be built and improved upon in any number of ways, so I hope that you enjoyed reading and using what you learned in your own projects. If you have any questions, comments, suggestions or ideas, please feel free to reach out to me. Thank you for reading.
Acknowledgments:
Thanks to John for catching some unnecessary code duplication! Catch out John's articles.
Top comments (3)
Nice article. Looks like the "evaluate" method in Condition and the "execute" method in Action are unused.
Interesting article. In your "complete definition of the Rule class" it looks like you accidentally duplicated your imports, or was that intentional for some reason?
Thanks for catching that code duplication; completely unintentional. I hope you don't mind, I threw up an acknowledgement and a link to your profile at the end of the article. I appreciate it, thank you!