jruizgit / rules

Durable Rules Engine
MIT License
1.16k stars 205 forks source link

How to create A && B || C rule #81

Closed MobileSolutionsPL closed 7 years ago

MobileSolutionsPL commented 7 years ago

Hi. I'm quite new to your rule engine, I'm trying to build my rule base on the family-tree description (let's assume it's very simple ontology here). I've extended the Kermit the frog example, also because the naming sounds like RDF graphs :) I've built such a code:

`@when_all(c.ismotherfact << (m.verb=='is mother of'), c.isfemalefact << (m.predicate=='female') & (c.ismotherfact.subject!=m.subject), c.isparentfact << (m.verb=='is parent') & (c.issexfact.subject==m.subject))

def isMother(c):
    if c.isparentfact!=None and len(c.isparentfact.subject)>0:
        c.assert_fact({'subject':c.isparentfact.subject, 'verb':'is mother of', 'predicate' : c.isparentfact.predicate})
    if c.ismotherfact!=None and len(c.ismotherfact.subject)>0:
        c.assert_fact({'subject':c.ismotherfact.subject, 'verb':'is', 'predicate':'female'})
        c.assert_fact({'subject':c.ismotherfact.subject, 'verb':'is parent', 'predicate':c.ismotherfact.predicate})

the business idea behind it is that "is mother of" relation may be set directly or inferred from "is parent" and "female" facts (someone who is a mother must be a parent and female).

the issue here is that in the background, there is infinite loop of calls on that rule. I've put log as a first line of the isMother method to check it. Am I doing something wrong here or it's kind of a bug in the framework?

Maybe there is kind of tutorial to the @when_all or @when_any syntax? for instance I've tried to create validationRuleFact where I've tried to compare c.isparentfact.subject with c.ismotherfact.subject but it fails somehow... could you please advise?

MobileSolutionsPL commented 7 years ago

Just quick note here, at the very beginning I've started with two methods - one for explicit relation and one for conjunction. Further I've assumed that adding existing relations to the fact database may cause problem, I've tried to combine it into one condition.

jruizgit commented 7 years ago

Hi, thanks for posting the question. Would you mind pasting the full code sample that leads to the infinite loop? Regarding the derivation of "is mother" from two facts "is parent" and "is female". This is how I interpret the rule:

from durable.lang import *

with ruleset('test'):
    @when_all(c.female << (m.verb == 'is') & (m.direct_object == 'female'),
              c.parent << (m.subject == c.female.subject) & (m.verb == 'is') & (m.direct_object == 'parent'))
    def isMother(c):
        c.assert_fact({ 'subject': c.parent.subject, 'verb': 'is', 'direct_object': 'mother', 'indirect_object': c.parent.indirect_object })

    @when_all(c.male << (m.verb == 'is') & (m.direct_object == 'male'),
              c.parent <<  (m.subject == c.male.subject) & (m.verb == 'is') & (m.direct_object == 'parent'))
    def isFather(c):
        c.assert_fact({ 'subject': c.parent.subject, 'verb': 'is', 'direct_object': 'father', 'indirect_object': c.parent.indirect_object })

    @when_all(+m.subject)
    def output(c):
        if not c.m.indirect_object:
            print('{0} {1} {2}'.format(c.m.subject, c.m.verb, c.m.direct_object))
        else:
            print('{0} {1} {2} of {3}'.format(c.m.subject, c.m.verb, c.m.direct_object, c.m.indirect_object))

    @when_start
    def start(c):
        c.assert_fact('test', { 'subject': 'Sally', 'verb': 'is', 'direct_object': 'female' })
        c.assert_fact('test', { 'subject': 'John', 'verb': 'is', 'direct_object': 'male' })
        c.assert_fact('test', { 'subject': 'Sally', 'verb': 'is', 'direct_object': 'parent', 'indirect_object': 'Alex' })
        c.assert_fact('test', { 'subject': 'John', 'verb': 'is', 'direct_object': 'parent', 'indirect_object': 'Alex' })

run_all()

What do you think? Is that what you have in mind?

MobileSolutionsPL commented 7 years ago

Hi. Thanks for the immediate reply. It covers first part of what I'm thinking about. It is true that female person who is a parent is mother (if A and B then C). But the relation can be reversed and we can infer that a mother is both a parent and a female (so if C then A and B are also true). And that causes the infinite loop. I've assumed that the reasoner will check that C or A and B facts are already inferred and won't check the rule again. But it seems they're calling each other.

Please check the edited code.

from durable.lang import * with ruleset('test'):

@when_all(c.female << (m.verb =='is') & (m.direct_object =='female'),
          c.parent << (m.subject == c.female.subject) & (m.verb =='is') & (m.direct_object =='parent'))
def isMother(c):
    c.assert_fact({ 'subject': c.parent.subject, 'verb': 'is', 'direct_object': 'mother', 'indirect_object': c.parent.indirect_object })

#infer sex and parent relationship from "is mother" relation
@when_all(m.direct_object=='mother')
def addNewFacts(c):
    c.assert_fact({ 'subject': c.m.subject, 'verb': 'is', 'direct_object': 'female' })
    c.assert_fact({ 'subject': c.m.subject, 'verb': 'is', 'direct_object': 'parent', 'indirect_object': c.m.indirect_object })

@when_all(+m.subject)
def output(c):
    if not c.m.indirect_object:
        print('{0} {1} {2}'.format(c.m.subject, c.m.verb, c.m.direct_object))
    else:
        print('{0} {1} {2} of {3}'.format(c.m.subject, c.m.verb, c.m.direct_object, c.m.indirect_object))

@when_start
def start(c):
    c.assert_fact('test', { 'subject': 'Sharon Marsh', 'verb': 'is', 'direct_object': 'female' })
    c.assert_fact('test', { 'subject': 'Sharon Marsh', 'verb': 'is', 'direct_object': 'parent', 'indirect_object': 'Stan' })

    c.assert_fact('test', { 'subject': 'Liane Cartman', 'verb': 'is', 'direct_object': 'mother', 'indirect_object': 'Eric' })

run_all()

jruizgit commented 7 years ago

Hi, thanks for clarifying. I understand the scenario now. Currently, every fact which doesn't have an 'id' property is considered a different fact. That is why you see the recursion. You can break the recursion by using the none() clause (see example below). This is a usability problem I'm intending to address (two facts with the same properties and values should be considered equivalent).

with ruleset('test'):
    @when_all(c.female << (m.verb == 'is') & (m.direct_object == 'female'),
              c.parent << (m.subject == c.female.subject) & (m.verb == 'is') & (m.direct_object == 'parent'),
              none((m.subject == c.female.subject) & (m.verb == 'is') & (m.direct_object == 'mother')))
    def isMother(c):
        c.assert_fact({ 'subject': c.parent.subject, 'verb': 'is', 'direct_object': 'mother', 'indirect_object': c.parent.indirect_object })

    @when_all((m.verb == 'is') & (m.direct_object == 'mother'))
    def addNewFacts(c):
        c.assert_fact({ 'subject': c.m.subject, 'verb': 'is', 'direct_object': 'female' })
        c.assert_fact({ 'subject': c.m.subject, 'verb': 'is', 'direct_object': 'parent', 'indirect_object': c.m.indirect_object })

    @when_all(+m.subject)
    def output(c):
        if not c.m.indirect_object:
            print('{0} {1} {2}'.format(c.m.subject, c.m.verb, c.m.direct_object))
        else:
            print('{0} {1} {2} of {3}'.format(c.m.subject, c.m.verb, c.m.direct_object, c.m.indirect_object))

    @when_start
    def start(c):
        c.assert_fact('test', { 'subject': 'Sally', 'verb': 'is', 'direct_object': 'female' })
        c.assert_fact('test', { 'subject': 'Sally', 'verb': 'is', 'direct_object': 'parent', 'indirect_object': 'Alex' })

run_all()
MobileSolutionsPL commented 7 years ago

Hi. That's perfect solution, regardless the way of implementation :) I've thought about negating and tried with none() function before, but I think I've done mistakes in the syntax.