Example #1
0
def test_agenda_has_activations():
    """ Agenda object has activations property """

    from pyknow.agenda import Agenda
    from collections import deque
    assert hasattr(Agenda(), "activations")
    assert isinstance(Agenda().activations, deque)
Example #2
0
def test_Agenda_get_next_returns_next_when_not_empty():
    from pyknow.agenda import Agenda

    a = Agenda()
    a.activations.append(True)

    assert a.get_next() is True
Example #3
0
    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
Example #4
0
 def __init__(self):
     self.context = Context()
     self._fixed_facts = []
     self.facts = FactList()
     self.running = False
     self.agenda = Agenda()
     self.strategy = self.__strategy__()
     self._parent = False
     self.shared_attributes = {}
Example #5
0
def test_agenda_get_next():
    """
    Agenda has a get_next method that gets from activations and inserts
    into executed
    """

    from pyknow.agenda import Agenda
    agenda = Agenda()

    agenda.activations.append("Foo")
    assert agenda.get_next() == "Foo"
    assert "Foo" not in agenda.activations
Example #6
0
    def reset(self):
        """
        Performs a reset as per CLIPS behaviour (resets the
        agenda and factlist and declares InitialFact())

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

        self.agenda = Agenda()
        self.facts = FactList()
        self.__declare(InitialFact())
        self.load_initial_facts()
        self.strategy.update_agenda(self.agenda, self.get_activations())
Example #7
0
def test_Depth_update_agenda_different_salience():
    from random import shuffle

    from pyknow.strategies import Depth
    from pyknow.activation import Activation
    from pyknow.rule import Rule
    from pyknow.agenda import Agenda

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

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

    st = Depth()
    a = Agenda()

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

    order = list(a.activations)
    assert (order.index(act4) < order.index(act3) < order.index(act2) <
            order.index(act1))
Example #8
0
    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")
Example #9
0
    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
Example #10
0
def test_Depth_update_agenda_asertion_order_affects_agenda_order_1():
    from pyknow.strategies import Depth
    from pyknow.activation import Activation
    from pyknow.rule import Rule
    from pyknow.agenda import Agenda

    act1 = Activation(rule=Rule(), facts=(1, ))
    act2 = Activation(rule=Rule(), facts=(2, ))
    first = {act1, act2}

    act3 = Activation(rule=Rule(), facts=(3, ))
    act4 = Activation(rule=Rule(), facts=(4, ))
    second = {act3, act4}

    a = Agenda()

    st = Depth()

    st.update_agenda(a, first)
    st.update_agenda(a, second)

    left, right = set(list(a.activations)[:2]), set(list(a.activations)[2:])

    assert left == second
    assert right == first
Example #11
0
def test_DepthStrategy_update_agenda_no_facts_returns_empty_agenda():
    from pyknow.strategies import DepthStrategy
    from pyknow.agenda import Agenda

    st = DepthStrategy()
    a = Agenda()

    st.update_agenda(a, [], [])

    assert not a.activations
Example #12
0
    def reset(self):
        """
        Performs a reset as per CLIPS behaviour (resets the
        agenda and factlist and declares InitialFact())

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

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

        self.matcher.reset()

        # Declare all deffacts
        for deffact in self.get_deffacts():
            for fact in deffact():
                self.__declare(fact)

        self.running = False
Example #13
0
    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")
Example #14
0
def test_Strategy_update_agenda_update_executed(strategy):
    from pyknow import strategies
    from pyknow.activation import Activation
    from pyknow.rule import Rule
    from pyknow.agenda import Agenda

    act1 = Activation(rule=Rule(), facts=(1, ))
    act2 = Activation(rule=Rule(), facts=(2, ))

    st = getattr(strategies, strategy)()
    a = Agenda()
    a.executed.add(act1)

    st.update_agenda(a, [act2])

    assert act1 not in a.executed
Example #15
0
def test_Depth_update_agenda_activations_to_agenda():
    from pyknow.strategies import Depth
    from pyknow.activation import Activation
    from pyknow.rule import Rule
    from pyknow.agenda import Agenda

    act1 = Activation(rule=Rule(), facts=(1, ))
    act2 = Activation(rule=Rule(), facts=(2, ))

    a = Agenda()

    st = Depth()
    st.update_agenda(a, {act1, act2})

    assert act1 in a.activations
    assert act2 in a.activations
Example #16
0
class KnowledgeEngine:

    __strategy__ = Depth

    def __init__(self):
        self._facts = FactList()
        self.agenda = Agenda()
        self.strategy = self.__strategy__()

    def declare(self, *facts):
        for fact in facts:
            idx = self._facts.declare(fact)
        self.strategy.update_agenda(self.agenda, self.get_activations())
        return idx

    def retract(self, idx):
        self._facts.retract(idx)
        self.strategy.update_agenda(self.agenda, self.get_activations())

    def get_rules(self):
        def _rules():
            for name, obj in getmembers(self):
                if isinstance(obj, Rule):
                    yield obj
        return list(_rules())

    def get_activations(self):
        def _activations():
            for rule in self.get_rules():
                for act in rule.get_activations(self._facts):
                    yield act
        return list(_activations())

    def run(self, steps=None):
        while steps is None or steps > 0:
            activation = self.agenda.get_next()
            if activation is None:
                break
            else:
                if steps is not None:
                    steps -= 1
                activation.rule(self)

    def reset(self):
        self.agenda = Agenda()
        self._facts = FactList()
        self.declare(InitialFact())
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 pyknow.strategies import DepthStrategy
    from pyknow.activation import Activation
    from pyknow import Rule
    from pyknow.agenda import Agenda
    from pyknow import Fact
    from pyknow.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))
Example #18
0
def test_DepthStrategy_update_agenda_different_salience():
    from random import shuffle

    from pyknow.strategies import DepthStrategy
    from pyknow.activation import Activation
    from pyknow import Rule
    from pyknow import Fact
    from pyknow.agenda import Agenda
    from pyknow.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))
Example #19
0
def test_DepthStrategy_update_agenda_assertion_order_affects_agenda_order_1():
    from pyknow.strategies import DepthStrategy
    from pyknow.activation import Activation
    from pyknow import Rule
    from pyknow.agenda import Agenda
    from pyknow import Fact
    from pyknow.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()

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

    st.update_agenda(a, second, first)
    assert list(a.activations) == list(reversed(second))
Example #20
0
def test_DepthStrategy_update_agenda_activations_to_agenda():
    from pyknow.strategies import DepthStrategy
    from pyknow.activation import Activation
    from pyknow import Rule
    from pyknow.agenda import Agenda
    from pyknow import Fact
    from pyknow.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
Example #21
0
def test_Agenda_get_next_adds_to_executed():
    from pyknow.agenda import Agenda
    from pyknow.rule import Rule
    from pyknow.activation import Activation
    from collections import deque

    act1 = Activation(rule=Rule(), facts=(1, ))
    act2 = Activation(rule=Rule(), facts=(2, ))
    
    a = Agenda()
    a.activations = deque([act1, act2])

    assert not act1 in a.executed
    assert not act2 in a.executed

    a.get_next()
    assert act1 in a.executed
    assert not act2 in a.executed

    a.get_next()
    assert act1 in a.executed
    assert act2 in a.executed
Example #22
0
class KnowledgeEngine:
    """
    This represents a clips' ``module``, wich is an ``inference engine``
    holding a set of ``rules`` (as :obj:`pyknow.rule.Rule` objects),
    an ``agenda`` (as :obj:`pyknow.agenda.Agenda` object)
    and a ``fact-list`` (as :obj:`pyknow.factlist.FactList` objects)

    This could be considered, when inherited from, as the
    ``knowlege-base``.
    """

    __strategy__ = Depth

    def __init__(self):
        self.context = Context()
        self._fixed_facts = []
        self.facts = FactList()
        self.running = False
        self.agenda = Agenda()
        self.strategy = self.__strategy__()
        self._parent = False
        self.shared_attributes = {}

    def __repr__(self):
        return "{}({})".format(self.__class__.__name__, self.shared_attributes)

    def set_shared_attributes(self, **shared_attributes):
        """
        Stablises a dict with shared attributes to be used
        by this KE's childs on a tree
        """

        self.shared_attributes.update(shared_attributes)

    @property
    def parent(self):
        """
        Parent Knowledge Engine. Used in tree-like KEs.
        :return: KnowledgeEngine
        """

        return self._parent

    @parent.setter
    def parent(self, parent):
        """
        Set a parent for later use.
        It must inherit from ``pyknow.engine.KnowledgeEngine``
        """

        if not isinstance(parent, KnowledgeEngine):
            raise ValueError("Parent must descend from KnowledgeEngine")

        self._parent = parent

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

        .. note::

            This updates the agenda.
        """

        if not self.running:
            logging.warning("Declaring fact while not run()")
        self.__declare(*facts)
        self.strategy.update_agenda(self.agenda, self.get_activations())

    def __declare(self, *facts):
        """
        Internal declaration method. Used for ``declare`` and ``deffacts``
        """

        def _declare_facts(facts):
            """ Declare facts """
            for fact in facts:
                for value in fact.value.values():
                    if not isinstance(value, L):
                        raise TypeError("Can only use ``L`` tipe on declare")
                yield self.facts.declare(fact)
        return list(_declare_facts(facts))

    def deffacts(self, *facts):
        """
        Declare a Fact from OUTSIDE the engine.
        Equivalent to clips' deffacts.
        """

        if self.running:
            logging.warning("Declaring fixed facts while run()")

        self._fixed_facts.extend(facts)

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

        .. note::
            This updates the agenda
        """

        idx = self.facts.retract(idx)
        self.agenda.remove_from_fact(idx)
        self.strategy.update_agenda(self.agenda, self.get_activations())

    def retract_matching(self, fact):
        """
        Retracts a specific fact, comparing against another fact

        .. note::
            This updates the agenda
        """

        for idx in self.facts.retract_matching(fact):
            self.agenda.remove_from_fact(idx)
        self.strategy.update_agenda(self.agenda, self.get_activations())

    def modify(self, fact, result_fact):
        """
        Modifies a fact.
        Facts are inmutable in Clips, thus, as documented in clips reference
        manual, this retracts a fact and then re-declares it
        """

        self.retract_matching(fact)
        self.declare(result_fact)

    def get_rules(self):
        """
        When instanced as a knowledge-base, this will return
        each of the rules that are assigned to it (the rule-base).
        """

        def _rules():
            for _, obj in getmembers(self):
                if isinstance(obj, Rule):
                    obj.ke = self
                    yield obj
        return list(_rules())

    def get_activations(self):
        """
        Matches the rule-base (see :func:`pyknow.engine.get_rules`)
        with the fact-list and returns each match
        """

        for rule in self.get_rules():
            capturations = rule.get_capturations(self.facts)
            for act in rule.get_activations(self.facts, capturations):
                if act:
                    act.rule = rule
                    yield act

        return

    def run(self, steps=None):
        """
        Execute agenda activations
        """

        self.running = True
        while steps is None or steps > 0:
            activation = self.agenda.get_next()
            if activation is None:
                break
            else:
                if steps is not None:
                    steps -= 1
                activation.rule(self, activation=activation)
        self.running = False

    def load_initial_facts(self):
        """
        Declares all fixed_facts
        """

        if self._fixed_facts:
            self.__declare(*self._fixed_facts)

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

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

        self.agenda = Agenda()
        self.facts = FactList()
        self.__declare(InitialFact())
        self.load_initial_facts()
        self.strategy.update_agenda(self.agenda, self.get_activations())
Example #23
0
 def __init__(self):
     self._facts = FactList()
     self.agenda = Agenda()
     self.strategy = self.__strategy__()
Example #24
0
 def reset(self):
     self.agenda = Agenda()
     self._facts = FactList()
     self.declare(InitialFact())
Example #25
0
def test_agenda_has_executed_set():
    """ Agenda object has executed property """

    from pyknow.agenda import Agenda
    assert hasattr(Agenda(), "executed")
    assert isinstance(Agenda().executed, set)
Example #26
0
class KnowledgeEngine:
    """
    This represents a clips' ``module``, wich is an ``inference engine``
    holding a set of ``rules`` (as :obj:`pyknow.rule.Rule` objects),
    an ``agenda`` (as :obj:`pyknow.agenda.Agenda` object)
    and a ``fact-list`` (as :obj:`pyknow.factlist.FactList` objects)

    This could be considered, when inherited from, as the
    ``knowlege-base``.
    """
    from pyknow.matchers import ReteMatcher as __matcher__
    from pyknow.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)
Example #27
0
def test_Agenda_get_next_returns_None_when_empty():
    from pyknow.agenda import Agenda

    a = Agenda()

    assert a.get_next() is None
Example #28
0
class KnowledgeEngine:
    """
    This represents a clips' ``module``, wich is an ``inference engine``
    holding a set of ``rules`` (as :obj:`pyknow.rule.Rule` objects),
    an ``agenda`` (as :obj:`pyknow.agenda.Agenda` object)
    and a ``fact-list`` (as :obj:`pyknow.factlist.FactList` objects)

    This could be considered, when inherited from, as the
    ``knowlege-base``.
    """
    from pyknow.matchers import ReteMatcher as __matcher__
    from pyknow.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)