Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjusting flowchart idea to fit stateless use case #366

Open
awilde27 opened this issue Jan 15, 2021 · 2 comments
Open

Adjusting flowchart idea to fit stateless use case #366

awilde27 opened this issue Jan 15, 2021 · 2 comments

Comments

@awilde27
Copy link

awilde27 commented Jan 15, 2021

Hi @jruizgit, brilliant package you've created here.

I have a use case in which I'm using durable_rules to a construct decision rule tree,
ideally where each node is its own ruleset and the further down the tree, the more sophisticated the consequents (in my case,
insights or recommendations). A flowchart seems like a great candidate, but the only catch is that my use case is
stateless, i.e. when I post an event I need it to flow through entirely rather than be queued to the next stage.

I'm still learning and thinking through the abilities of durable_rules, but my intuition tells me I need something between
a set of rulesets and a flowchart, and something in between a fact assertion and an event posting.

What I'd appreciate insight on:

  1. Does the example below seem like an antipattern?
  2. Is there a way other than a callback to an API or global variable, as examples, to create an in-memory stack within the state or
    somewhere else to capture/record all triggered rules? I've read through How to get result back from cUrl #116 and also tried to create a state variable, c.s.result_stack
    which seemed to work until I posted an event to a ruleset which posted that same event to a third.
  3. Based on the docs, is there a way to force an event to pass through a flowchart without posting multiple events?

For this dummy example, you can assume I'm posting data that contains the same k attributes each time, uniqueness exists for
item_name.

Thank you in advance.

import pandas as pd
from durable.lang import *

def post_in_rule(ruleset_name, c):
    post_d = dict(c.m.items())
    try:
        # redirect event to ruleset `ruleset_name`
        result = post(ruleset_name, post_d)
        # grab rule name and result from current state after passing through ruleset `ruleset_name`
        rule_name, consequent = result.get('rule_name'), result.get('result')
        stack_payload = {'item': c.m.item_name, 'action_dt': c.m.action_dt,
                         'ruleset': ruleset_name, 'rule_name': rule_name, 'result': consequent}
        if c.s.result_stack is None:
            c.s.result_stack = []
        c.s.result_stack.append(stack_payload)
    except Exception as e:
        if isinstance(e, MessageNotHandledException):
            print('Item was posted to ruleset "{}" and did not trigger any rules'.format(ruleset_name))
            pass
        else:
            raise e

def add_event_capture(c, rule_name, consequent):
    c.s.result = consequent
    c.s.rule_name = rule_name
    ruleset_name = c._ruleset._name
    stack_payload = {'item': c.m.item_name, 'action_dt': c.m.action_dt,
                     'ruleset': ruleset_name, 'rule_name': rule_name, 'result': consequent}
    if c.s.result_stack is None:
        c.s.result_stack = []
    c.s.result_stack.append(stack_payload)


with ruleset('report'):
    @when_all(m.days_old > 100)
    def report_out_of_date(c):
        res = '{} out of date report ({} days old)'.format(c.m.item_name, int(c.m.days_old))
        print(res)
        # record the rule was triggered, originally recording metadata to c.s.result_stack
        add_event_capture(c, 'report_out_of_date', res)

    @when_all(m.num_references > 10)
    def large_num_references(c):
        res = '{} has large number of references, prioritize (references:{})'.format(c.m.item_name, int(c.m.num_references))
        print(res)
        add_event_capture(c, 'large_num_references', res)

with ruleset('classify_due_item'):
    @when_all(m.item_type == 'report')
    def is_report(c):
        res = 'Item {} is a report'.format(c.m.item_name)
        print(res)
        add_event_capture(c, 'diff_exp', res)
        # post to other ruleset, i.e. move down condition tree
        post_to_ruleset('report', c)

    @when_all(m.item_type != 'report')
    def not_report(c):
        res = '{} is not report, but alert team'.format(c.m.item_name)
        print(res)
        add_event_capture(c, 'not_report', res)


with ruleset('action_item'):
    # all items action_dt >= `today`
    @when_all(m.action_dt <= 1610746698 + 604881)  # within a week from today
    def due_soon(c):
        res = 'Item {} needs attention by {}'.format(c.m.item_name, pd.datetime.from_timestamp(c.m.action_dt).strftime('%Y-%m-%d'))
        print(res)
        add_event_capture(c, 'due_soon', res)
        post_to_ruleset('classify_due_item', c)

    @when_all(m.action_dt > 1610746698 + 604881)
    def due_later(c):
        res = '{} due later, put on backlog (due date: {})'.format(
            c.m.item_name,
            pd.datetime.from_timestamp(c.m.action_dt).strftime('%Y-%m-%d')
        )
        print(res)
        add_event_capture(c, 'due_later', res)

d = {'item_name': 'Report 123', 'item_type': 'report', 'action_dt': 1611146698, 'num_references': 18, 'days_old': 23}
post('action_item', d)
@jruizgit
Copy link
Owner

Hi, thanks for posting the question. There is no good or wrong answer. But, I think your example would be more efficient if you write all the rules in a single ruleset and use forward chaining by making assertions.

Below is a very simple example: asserting "Kermit eats flies" and "Kermit lives in water" will trigger the assertion "Kermit is a frog", which will trigger the assertion "Kermit is green".

Internally the rules engine will remember the facts you have asserted by building a decision tree (Rete), the decision to assert "Kermit is a frog" right after asserting "Kermit lives in water" is optimal as all the facts don't need to be re-evaluated again.

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):
        c.assert_fact({ 'subject': c.first.subject, 'predicate': 'is', 'object': 'frog' })

    @when_all((m.predicate == 'is') & (m.object == 'frog'))
    def green(c):
        c.assert_fact({ 'subject': c.m.subject, 'predicate': 'is', 'object': 'green' })

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

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

Hope this helps.

@awilde27
Copy link
Author

Thank you for the quick response. I understand the idea here that I can forward-chain assertions. Given your example, I think the additional element I have to incorporate here is to forward chain specific attributes of c.m so that when a rule asserts a new fact I only include relevant data so that a rule I wish to trigger lower in the set gets triggered as opposed to getting caught in an infinite loop. What I find tricky is organizing a ruleset for scale and readability. Nevertheless, it might be a bit more overhead on my part, but agreed it is a more efficient implementation.

I think it makes sense, within a rule, to assert a fact out to a separate ruleset dedicated to simply for storing consequents triggered within the main ruleset. This seems necessary, as the engine will only store the data that a fact passed to the ruleset as opposed to the consequent. Will try this approach and follow up with any further questions - appreciate the insight.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants