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
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
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
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())
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)
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())
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)
def test_Agenda_get_next_returns_None_when_empty(): from pyknow.agenda import Agenda a = Agenda() assert a.get_next() is None