def reset(self, **kwargs):
        """
        Performs a reset as per CLIPS behaviour (resets the
        agenda and factlist and declares InitialFact())

        Any keyword argument passed to `reset` will be passed to @DefFacts
        which have those arguments on their signature.

        .. note:: If persistent facts have been added, they'll be
                  re-declared.
        """

        self.agenda = Agenda()
        self.facts = FactList()

        self.matcher.reset()

        deffacts = []
        for deffact in self.get_deffacts():
            signature = inspect.signature(deffact)
            if not any(p.kind == inspect.Parameter.VAR_KEYWORD
                       for p in signature.parameters.values()):
                # There is not **kwargs defined. Pass only the defined
                # names.
                args = set(signature.parameters.keys())
                deffacts.append(
                    deffact(**{k: v
                               for k, v in kwargs.items() if k in args}))
            else:
                deffacts.append(deffact(**kwargs))

        # Declare all facts yielded by deffacts
        self.__declare(*chain.from_iterable(deffacts))

        self.running = False
Beispiel #2
0
def test_factlist_declare():
    """ Test declare method adds to factlist and updates index """
    from experta.factlist import FactList
    from experta import Fact
    flist = FactList()
    assert getattr(flist, "last_index") == 0
    assert not flist
    flist.declare(Fact())
    assert getattr(flist, "last_index") == 1
    assert isinstance(flist[0], Fact)
Beispiel #3
0
def test_factlist_raises_valueerror_on_invalid_fact():
    from experta.factlist import FactList
    from experta import Fact, Field

    class MockFact(Fact):
        must_be_string = Field(str, mandatory=True)

    flist = FactList()
    f0 = MockFact(must_be_string=0)

    with pytest.raises(ValueError):
        flist.declare(f0)
Beispiel #4
0
def test_factlist_retract():
    """ Test retract method """

    from experta.factlist import FactList
    from experta import Fact
    flist = FactList()
    assert getattr(flist, "last_index") == 0
    assert not flist
    flist.declare(Fact())
    assert getattr(flist, "last_index") == 1
    assert isinstance(flist[0], Fact)
    assert flist.retract(0) == 0
    assert not flist
    def __init__(self):
        self.running = False
        self.facts = FactList()
        self.agenda = Agenda()

        if (isinstance(self.__matcher__, type)
                and issubclass(self.__matcher__, abstract.Matcher)):
            self.matcher = self.__matcher__(self)
        else:
            raise TypeError("__matcher__ must be a subclass of Matcher")

        if (isinstance(self.__strategy__, type)
                and issubclass(self.__strategy__, abstract.Strategy)):
            self.strategy = self.__strategy__()
        else:
            raise TypeError("__strategy__ must be a subclass of Strategy")
Beispiel #6
0
def test_factlist_declare_raises_valueError():
    """ declare raises valueerror if not ``Fact`` object providen """

    from experta.factlist import FactList
    import pytest

    with pytest.raises(ValueError):
        FactList().declare("Foo")
Beispiel #7
0
def test_DepthStrategy_update_agenda_different_salience():
    from random import shuffle

    from experta.strategies import DepthStrategy
    from experta.activation import Activation
    from experta import Rule
    from experta import Fact
    from experta.agenda import Agenda
    from experta.factlist import FactList

    flist = FactList()

    f1 = Fact(1)
    flist.declare(f1)

    f2 = Fact(2)
    flist.declare(f2)

    f3 = Fact(3)
    flist.declare(f3)

    f4 = Fact(4)
    flist.declare(f4)

    act1 = Activation(rule=Rule(salience=1), facts=(f1, ))
    act2 = Activation(rule=Rule(salience=2), facts=(f2, ))
    act3 = Activation(rule=Rule(salience=3), facts=(f3, ))
    act4 = Activation(rule=Rule(salience=4), facts=(f4, ))

    acts = [act1, act2, act3, act4]
    shuffle(acts)

    st = DepthStrategy()
    a = Agenda()

    for act in acts:
        st.update_agenda(a, acts, [])

    order = list(a.activations)
    assert (order.index(act4)
            > order.index(act3)
            > order.index(act2)
            > order.index(act1))
Beispiel #8
0
def test_DepthStrategy_update_agenda_assertion_order_affects_agenda_order_1():
    from experta.strategies import DepthStrategy
    from experta.activation import Activation
    from experta import Rule
    from experta.agenda import Agenda
    from experta import Fact
    from experta.factlist import FactList

    fl = FactList()

    f1 = Fact(1)
    fl.declare(f1)

    f2 = Fact(2)
    fl.declare(f2)

    f3 = Fact(3)
    fl.declare(f3)

    f4 = Fact(4)
    fl.declare(f4)

    act1 = Activation(rule=Rule(), facts=(f1, ))
    act2 = Activation(rule=Rule(), facts=(f2, ))
    first = [act1, act2]

    act3 = Activation(rule=Rule(), facts=(f3, ))
    act4 = Activation(rule=Rule(), facts=(f4, ))
    second = [act3, act4]

    a = Agenda()

    st = DepthStrategy()

    st.update_agenda(a, first, [])
    assert a.activations == first

    st.update_agenda(a, second, first)
    assert a.activations == second
Beispiel #9
0
def test_factlist_changes():
    """ Test factlist changes """

    from experta.factlist import FactList
    from experta import Fact

    flist = FactList()

    f0 = flist.declare(Fact(a=1))
    assert flist.changes[0] == [f0]

    f1 = flist.declare(Fact(b=1))
    assert flist.changes[0] == [f1]

    flist.retract(f1)
    assert flist.changes[1] == [f1]
Beispiel #10
0
def test_DepthStrategy_update_agenda_asertion_order_affects_agenda_order_3():
    """

    From Clips docs on Depth Strategy::

      Newly activated rules are placed above all rules of the same salience.
      For example, given that facta activates rule1 and rule2 and factb
      activates rule3 and rule4, then if facta is asserted before factb, rule3
      and rule4 will be above rule1 and rule2 on the agenda. However, the
      position of rule1 relative to rule2 and rule3 relative to rule4 will be
      arbitrary.

    """
    from experta.strategies import DepthStrategy
    from experta.activation import Activation
    from experta import Rule
    from experta.agenda import Agenda
    from experta import Fact
    from experta.factlist import FactList

    fl = FactList()

    f1 = Fact(1)
    fl.declare(f1)

    f2 = Fact(2)
    fl.declare(f2)

    act1 = Activation(rule=Rule(), facts=(f1, ))
    act2 = Activation(rule=Rule(), facts=(f1, ))
    act3 = Activation(rule=Rule(), facts=(f2, ))
    act4 = Activation(rule=Rule(), facts=(f2, ))

    a = Agenda()

    st = DepthStrategy()

    st.update_agenda(a, [act1, act2, act3, act4], [])
    order = list(a.activations)

    assert (order.index(act4) > order.index(act1)
            and order.index(act4) > order.index(act2))
    assert (order.index(act3) > order.index(act1)
            and order.index(act3) > order.index(act2))
Beispiel #11
0
def test_DepthStrategy_update_agenda_activations_to_agenda():
    from experta.strategies import DepthStrategy
    from experta.activation import Activation
    from experta import Rule
    from experta.agenda import Agenda
    from experta import Fact
    from experta.factlist import FactList

    fl = FactList()
    f1 = Fact(1)
    fl.declare(f1)
    f2 = Fact(2)
    fl.declare(f2)

    act1 = Activation(rule=Rule(), facts=(f1, ))
    act2 = Activation(rule=Rule(), facts=(f2, ))

    a = Agenda()

    st = DepthStrategy()
    st.update_agenda(a, [act1, act2], [])

    assert act1 in a.activations
    assert act2 in a.activations
Beispiel #12
0
def test_factlist_facts_idx_starts_zero():
    """ Factlist idx starts at zero """

    from experta.factlist import FactList
    assert getattr(FactList(), "last_index") == 0
class KnowledgeEngine:
    """
    This represents a clips' ``module``, wich is an ``inference engine``
    holding a set of ``rules`` (as :obj:`experta.rule.Rule` objects),
    an ``agenda`` (as :obj:`experta.agenda.Agenda` object)
    and a ``fact-list`` (as :obj:`experta.factlist.FactList` objects)

    This could be considered, when inherited from, as the
    ``knowlege-base``.
    """
    from experta.matchers import ReteMatcher as __matcher__
    from experta.strategies import DepthStrategy as __strategy__

    def __init__(self):
        self.running = False
        self.facts = FactList()
        self.agenda = Agenda()

        if (isinstance(self.__matcher__, type)
                and issubclass(self.__matcher__, abstract.Matcher)):
            self.matcher = self.__matcher__(self)
        else:
            raise TypeError("__matcher__ must be a subclass of Matcher")

        if (isinstance(self.__strategy__, type)
                and issubclass(self.__strategy__, abstract.Strategy)):
            self.strategy = self.__strategy__()
        else:
            raise TypeError("__strategy__ must be a subclass of Strategy")

    @staticmethod
    def _get_real_modifiers(**modifiers):
        for k, v in modifiers.items():
            if k.startswith('_') and k[1:].isnumeric():
                yield (int(k[1:]), v)
            else:
                yield (k, v)

    def modify(self, declared_fact, **modifiers):
        """

        Modifies a fact.

        Facts are inmutable in Clips, thus, as documented in clips
        reference manual, this retracts a fact and then re-declares it

        `modifiers` must be a Mapping object containing keys and values
        to be changed.

        To allow modifying positional facts, the user can pass a string
        containing the symbol "_" followed by the numeric index
        (starting with 0). Ex::

            >>> ke.modify(my_fact, _0="hello", _1="world", other_key="!")

        """
        self.retract(declared_fact)

        newfact = declared_fact.copy()
        newfact.update(dict(self._get_real_modifiers(**modifiers)))

        return self.declare(newfact)

    def duplicate(self, template_fact, **modifiers):
        """Create a new fact from an existing one."""

        newfact = template_fact.copy()
        newfact.update(dict(self._get_real_modifiers(**modifiers)))

        return self.declare(newfact)

    @DefFacts(order=-1)
    def _declare_initial_fact(self):
        yield InitialFact()

    def _get_by_type(self, wanted_type):
        for _, obj in inspect.getmembers(self):
            if isinstance(obj, wanted_type):
                obj.ke = self
                yield obj

    def get_rules(self):
        """Return the existing rules."""
        return list(self._get_by_type(Rule))

    def get_deffacts(self):
        """Return the existing deffacts sorted by the internal order"""
        return sorted(self._get_by_type(DefFacts), key=lambda d: d.order)

    def get_activations(self):
        """
        Return activations
        """
        return self.matcher.changes(*self.facts.changes)

    def retract(self, idx_or_declared_fact):
        """
        Retracts a specific fact, using its index

        .. note::
            This updates the agenda
        """
        self.facts.retract(idx_or_declared_fact)

        if not self.running:
            added, removed = self.get_activations()
            self.strategy.update_agenda(self.agenda, added, removed)

    def run(self, steps=float('inf')):
        """
        Execute agenda activations
        """

        self.running = True
        activation = None
        execution = 0
        while steps > 0 and self.running:

            added, removed = self.get_activations()
            self.strategy.update_agenda(self.agenda, added, removed)

            if watchers.worth('AGENDA', 'DEBUG'):  # pragma: no cover
                for idx, act in enumerate(self.agenda.activations):
                    watchers.AGENDA.debug("%d: %r %r", idx, act.rule.__name__,
                                          ", ".join(str(f) for f in act.facts))

            activation = self.agenda.get_next()

            if activation is None:
                break
            else:
                steps -= 1
                execution += 1

                watchers.RULES.info(
                    "FIRE %s %s: %s", execution, activation.rule.__name__,
                    ", ".join(str(f) for f in activation.facts))

                activation.rule(
                    self, **{
                        k: v
                        for k, v in activation.context.items()
                        if not k.startswith('__')
                    })

        self.running = False

    def halt(self):
        self.running = False

    def reset(self, **kwargs):
        """
        Performs a reset as per CLIPS behaviour (resets the
        agenda and factlist and declares InitialFact())

        Any keyword argument passed to `reset` will be passed to @DefFacts
        which have those arguments on their signature.

        .. note:: If persistent facts have been added, they'll be
                  re-declared.
        """

        self.agenda = Agenda()
        self.facts = FactList()

        self.matcher.reset()

        deffacts = []
        for deffact in self.get_deffacts():
            signature = inspect.signature(deffact)
            if not any(p.kind == inspect.Parameter.VAR_KEYWORD
                       for p in signature.parameters.values()):
                # There is not **kwargs defined. Pass only the defined
                # names.
                args = set(signature.parameters.keys())
                deffacts.append(
                    deffact(**{k: v
                               for k, v in kwargs.items() if k in args}))
            else:
                deffacts.append(deffact(**kwargs))

        # Declare all facts yielded by deffacts
        self.__declare(*chain.from_iterable(deffacts))

        self.running = False

    def __declare(self, *facts):
        """
        Internal declaration method. Used for ``declare`` and ``deffacts``
        """
        if any(f.has_field_constraints() for f in facts):
            raise TypeError(
                "Declared facts cannot contain conditional elements")
        elif any(f.has_nested_accessor() for f in facts):
            raise KeyError(
                "Cannot declare facts containing double underscores as keys.")
        else:
            last_inserted = None
            for fact in facts:
                last_inserted = self.facts.declare(fact)

            if not self.running:
                added, removed = self.get_activations()
                self.strategy.update_agenda(self.agenda, added, removed)

            return last_inserted

    def declare(self, *facts):
        """
        Declare from inside a fact, equivalent to ``assert`` in clips.

        .. note::

            This updates the agenda.
        """

        if not self.facts:
            watchers.ENGINE.warning("Declaring fact before reset()")
        return self.__declare(*facts)