Durable rules (persistent rules engine) learning notes

1. Write in front

This article records the process of learning durable rules. The persistent rule engine is a multilingual framework for creating a rule-based system that can reason about a large number of factual statements . In vernacular, it is to formulate some rules in advance. These rules describe to Matched events and actions taken. In this way, when a fact comes, you can match the event and take corresponding actions. Very similar to the logic written in our code if...else if..else.

So why use this thing? Fast, and suitable for multiple languages, and if there are too many judgments, it is not as bloated as if else. The written judgment statement is more elegant.

Of course, since I am just learning and there are not many reference documents, I will use the github project to sort out how to use it through 3 demos, and finally think of a demo by myself to sort out how to use it in actual use. Seeing this operation used by colleagues in the company still feels very annoying, so it's like learning.

Scenes:

For example, the conditions for a girl to find a partner: she is handsome, owns a house, has a high income, and is a teacher or programmer, so she will date him. Then suppose we have many boys with the above attributes , and finally judge the girl's attitude towards him.

At this time, suppose there is a boy's data: {"name": "zhangsan", "age": 25, "job": "teacher", "salary": 2000, "appearance": "good"}, Let's judge the girl's final attitude, like, average, and dislike.

The demo is compiled by myself. This is a more conventional way of writing if statements, if ... then ....but when there are many conditional judgments, this way of writing is very cumbersome and bloated, and it is not elegant, so if the girl chooses the object After the standard is fixed, we can write a rule, and when there is new data to judge in the future, we only need to call this rule.

2. Usage demo understanding

2.1 The most basic

Here is a summary of the demo understanding in the link above.

Define rules that describe the pattern of events or facts to match (precendant) and the action to take (follower)

First, install the package:

pip install durable_rules

I found here that if pyenv is installed in the system, and then conda is installed, I find that using pip directly will not package the package into the conda environment, but the pyenv environment, so I must enter the specific conda virtual environment here Environment, and then call the pip inside it. Otherwise, running jupyter in conda will report an error that the package cannot be found, because direct pip will package it into pyenv. This has to be paid attention to. Of course, direct pip without pyenv will do.

from durable.lang import *

# 规则名字
with ruleset('test'):
		# 这东西如果匹配上一个,就不往下走了
        # antecedent 先行条件  
        @when_all(m.name == 'wuzhongqiang')   # 这里面的字段就是传入的数据里面有的字段
        def say_hello(c):
            # consequent 后续
            print ('Hello {}'.format(c.m.name))
        
        @when_all(m.age > 18)
        def young(c):
            print("{} 成年了".format(c.m.name))
        
        @when_all(m.sex == "boy")
        def sex(c):
            print("{} 是个男孩".format(c.m.name))
        
        # 兜底的功能,也就是上面这些都不匹配的时候, 就走最下面这个
        @when_all(+m.sex)
        def output(c):
            print(c.m.name, c.m.age, c.m.sex)

This is the most basic and most commonly used method, and the grammar is relatively simple. First define a rule name, and then start writing the antecedent condition, and the behavior triggered after the antecedent condition is met.

# 这个匹配第一个先行条件  
post('test', {
    
     'name': 'wuzhongqiang', 'age': 25, 'sex': 'boy'})
## Hello wuzhongqiang  也就是匹配到一个先行条件,执行之后,就不忘后面走了

# 匹配第二个先行条件
post('test', {
    
     'name': 'zhongqiang', 'age': 25, 'sex': 'boy'})
# zhongqiang 成年了

# 不匹配所有先行条件,就走最后一个兜底策略
post('test', {
    
     'name': 'zhongqiang', 'age': 16, 'sex': 'girl'})
# zhongqiang 16 girl

This logic is very simple, needless to say, but there are two points to note:

  1. The rule name can only be used once, that is, the code defined in the above rule. If it is defined repeatedly, an error will be reported, indicating that the rule name has been registered, and an exception will be reported.
  2. There must be a bottom-up behavior, otherwise, assuming that the incoming data does not match the preceding conditions, an error will be reported and the exception cannot be handled

2.2 assert_fact

This way of writing is a bit complicated. To be honest, I don’t know when it will be used, but I can probably understand his example.

This thing is used for forward reasoning, similar to a syllogism:

  • If P, then Q
  • P appears
  • Therefore, Q

The example he gave, in fact, I didn’t understand it at first, but after debugging later, I almost understood it.

from durable.lang import *
with ruleset('animal'):
        @when_all(c.first << (m.predicate == 'eats') & (m.object == 'flies'),
                  (m.predicate == 'lives') & (m.object == 'water') & (m.subject == c.first.subject))
        def frog(c):
            print(c.first.subject)
            c.assert_fact({
    
     'subject': c.first.subject, 'predicate': 'is', 'object': 'frog' })

        @when_all(c.first << (m.predicate == 'eats') & (m.object == 'flies'),
                  (m.predicate == 'lives') & (m.object == 'land') & (m.subject == c.first.subject))
        def chameleon(c):
            c.assert_fact({
    
     'subject': c.first.subject, 'predicate': 'is', 'object': 'chameleon' })

        @when_all(+m.subject)
        def output(c):
            print('Fact: {0} {1} {2}'.format(c.m.subject, c.m.predicate, c.m.object))

In fact, the prerequisites are defined here. For example, I run the following line of code:

assert_fact('animal', {
    
     'subject': 'Kermit', 'predicate': 'eats', 'object': 'flies' })

This will output Fact: Kermit eats flies, because the thing passed in does not meet any of the preceding conditions (because it is only itself), but after running this, next, if you run the following line of code:

assert_fact('animal', {
    
     'subject': 'Kermit', 'predicate': 'lives', 'object': 'water' })

The output of this:

Kermit
Fact: Kermit is frog
Fact: Kermit lives water

This is because, with a prerequisite (c.first), that is, the first line of code to run, and then run the following, it just matches the first prerequisite

        @when_all(c.first << (m.predicate == 'eats') & (m.object == 'flies'),
                  (m.predicate == 'lives') & (m.object == 'water') & (m.subject == c.first.subject))
        def frog(c):
            print(c.first.subject)
            c.assert_fact({
    
     'subject': c.first.subject, 'predicate': 'is', 'object': 'frog' })

Then output them in output. The above result came out.

And also found a phenomenon, if run first

assert_fact('animal', {
    
     'subject': 'Kermit', 'predicate': 'lives', 'object': 'water' })

will only output Fact: Kermit lives water, but on this basis, then run

assert_fact('animal', {
    
     'subject': 'Kermit', 'predicate': 'eats', 'object': 'flies' })

This will also output:

Kermit
Fact: Kermit is frog
Fact: Kermit eats flies

It turned out to be the above logic, I was very puzzled, I don't know why.

But in this usage scenario, I understand that there will be an antecedent condition that occurs first. After it occurs, the matching is performed. If the prerequisite condition is matched, the corresponding statement will be executed.

2.3 Pattern matching

This is the most basic usage above

from durable.lang import *

with ruleset('pipei'):
    @when_all(m.subject.matches('3[47][0-9]{13}'))
    def amex(c):
        print ('Amex detected {0}'.format(c.m.subject))

    @when_all(m.subject.matches('4[0-9]{12}([0-9]{3})?'))
    def visa(c):
        print ('Visa detected {0}'.format(c.m.subject))

    @when_all(m.subject.matches('(5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|2720)[0-9]{12}'))
    def mastercard(c):
        print ('Mastercard detected {0}'.format(c.m.subject))

So when using it, if the conditions are mutually exclusive, we can write it under the same rule.

3. demo

After a whole morning of groping, I found that this thing is not very difficult, and it is very similar to if...else. Let's see how to use it through the scene demo at the beginning.

In actual use, we generally write the rule engine as a class

class RuleEngine:
    def __init__(self, rule_name: str = ''):
        if not rule_name:
            raise ValueError("rule_name is empty")
        self.rule_name = rule_name

    def get_result(self, data: dict = None):
        if not data:
            raise ValueError("data is empty")

        output = None
        for _ in range(3):
            output = post(self.rule_name, data)
            if not output is None:
                break

        if not output is None:
            return output.get('result', {
    
    })
        else:
            print("get_result, return None: {}".format(data))
            return {
    
    }

Pass in the rule name and create an object

attitude_rule = RuleEngine('attitude')

create a new data

person1 = {
    
    "name": "zhangsan", "age": 25, "job": "teacher", "salary": 2000, "appearance": "good"}

Writing rules, this step is the core

with ruleset("attitude"):
    @when_all(
        (m.age < 30)
        & ((m.salary > 10000) | (m.appearance == "good")) & ((m.job == "teacher") | (m.job == "manong"))
    )
    def like(c):
        c.s.result = {
    
    
            'attitude_res': 'like',
            }
        
    @when_all(
        (m.age < 30)
        & ((m.salary <= 10000) | (m.appearance == "medium")) & ((m.job == "teacher") | (m.job == "manong"))
    )
    def yiban(c):
        c.s.result = {
    
    
            'attitude_res': 'yiban',
            }
        
    @when_all(+m._age)
    def other(c):
        c.s.result = {
    
    
            'attitude_res': 'dislike'
        }

Then call the get_result function to get the result

attitude_res = attitude_rule.get_result(person1)
# {'attitude_res': 'like'}

update this to person

person1.update(attitude_res)

person1: 
{
    
    'name': 'zhangsan',
 'age': 25,
 'job': 'teacher',
 'salary': 2000,
 'appearance': 'good',
 'attitude_res': 'like'}

This thing feels that it can also be used when making a new feature column.

That's all for this exploration. Since there are too few reference materials, I will come back to make up when I encounter new ones later.

reference

Guess you like

Origin blog.csdn.net/wuzhongqiang/article/details/125975773