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 test_FactList_retract_unknown_index_raise_IndexError(idx): from pyknow.factlist import FactList fl = FactList() with pytest.raises(IndexError): fl.retract(idx)
def test_FactList_declare_allow_Fact_instances(): from pyknow.factlist import FactList from pyknow.fact import Fact f = Fact() fl = FactList() fl.declare(f)
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 test_FactList_declare_returns_None_if_fact_already_exists(): from pyknow.factlist import FactList from pyknow.fact import Fact f0 = f1 = Fact(data=1) fl = FactList() assert fl.declare(f0) == 0 assert fl.declare(f1) is None
def test_factlist_declare(): """ Test declare method adds to factlist and updates index """ from pyknow.factlist import FactList from pyknow 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)
def test_NOT_match_InitialFact_if_fact_is_not_present(): from pyknow.rule import NOT from pyknow.factlist import FactList from pyknow.fact import Fact, InitialFact r = NOT(Fact(something=True)) fl = FactList() fl.declare(InitialFact()) assert r.get_activations(fl)
def test_FactList_declare_allow_Fact_subclasses(): from pyknow.factlist import FactList from pyknow.fact import Fact class OtherFact(Fact): pass f = OtherFact() fl = FactList() fl.declare(f)
def test_FactList_declare_returns_idx(): from pyknow.factlist import FactList from pyknow.fact import Fact f0 = Fact(data=1) f1 = Fact(data=2) fl = FactList() assert fl.declare(f0) == 0 assert fl.declare(f1) == 1
def test_Rule_empty_matches_with_initial_fact(): from pyknow.rule import Rule from pyknow.factlist import FactList from pyknow.fact import InitialFact from pyknow.activation import Activation r = Rule() fl = FactList() idx = fl.declare(InitialFact()) assert Activation(r, (0,)) in r.get_activations(fl)
def test_Rule_with_only_one_NOT_match_InitialFact_if_fact_is_not_present(): from pyknow.rule import NOT, Rule from pyknow.factlist import FactList from pyknow.fact import Fact, InitialFact, L from pyknow.match import Capturation r = Rule(NOT(Fact(something=L(True)))) fl = FactList() fl.declare(InitialFact()) assert next(r.get_activations(fl, Capturation()), None) is not None
def test_facts_produce_activations_without_capturation(): """ A fact produces activations if no capturations are provided """ from pyknow.fact import Fact, L from pyknow.factlist import FactList from pyknow.match import Capturation flist = FactList() flist.declare(Fact(a=L(1))) caps = list(Fact(a=L(1)).get_activations(flist, Capturation())) assert len(caps) == 1
def test_Rule_empty_matches_with_initial_fact(): from pyknow.rule import Rule from pyknow.factlist import FactList from pyknow.fact import InitialFact from pyknow.activation import Activation from pyknow.match import Capturation r = Rule() fl = FactList() fl.declare(InitialFact()) assert Activation(None, (0, )) in list(r.get_activations(fl, Capturation()))
def test_factlist_raises_valueerror_on_invalid_fact(): from pyknow.factlist import FactList from pyknow 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)
def test_Rule_multiple_criteria_generates_activation_with_matching_facts(): from pyknow.rule import Rule from pyknow.factlist import FactList from pyknow.fact import Fact, L r = Rule(Fact(a=L(1)), Fact(b=L(2))) fl = FactList() fl.declare(Fact(a=L(1))) fl.declare(Fact(b=L(2))) activations = list(r.get_activations(fl)) assert len(activations) == 1 assert {0, 1} == set(activations[0].facts)
def test_factlist_retract_matching(): """ Test retract_matching method """ from pyknow.factlist import FactList from pyknow.fact import Fact flist = FactList() assert getattr(flist, "_fidx") == 0 assert not flist.facts flist.declare(Fact()) assert getattr(flist, "_fidx") == 1 assert isinstance(flist.facts[0], Fact) assert flist.retract_matching(Fact()) == [0] assert not flist.facts
def test_FactList_matches(): from pyknow.factlist import FactList from pyknow.fact import Fact f = Fact() f0 = Fact(something=True) f1 = Fact(something=False) fl = FactList() fl.declare(f0) fl.declare(f1) assert fl.matches(f) == [0, 1]
def test_factlist_retract(): """ Test retract method """ from pyknow.factlist import FactList from pyknow 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 test_Rule_and_AND_nesting(): from pyknow.rule import Rule, AND from pyknow.factlist import FactList from pyknow.fact import Fact, L r = Rule(AND(Fact(a=L(2)), Fact(b=L(1)))) fl = FactList() fl.declare(Fact(a=L(2))) fl.declare(Fact(b=L(1))) activations = list(r.get_activations(fl)) assert len(activations) == 1 assert {0, 1} == set(activations[0].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())
def test_facts_produce_activations_that_are_Activations(): """ A fact produces activations that are Activation objects """ from pyknow.fact import Fact, L from pyknow.factlist import FactList from pyknow.match import Capturation from pyknow.activation import Activation flist = FactList() flist.declare(Fact(a=L(1))) caps = list(Fact(a=L(1)).get_activations(flist, Capturation())) assert len(caps) == 1 assert isinstance(caps[0], Activation) assert caps[0].facts == (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")
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_Rule_empty_doesnt_match_empty_factlist(): from pyknow.rule import Rule from pyknow.factlist import FactList r = Rule() fl = FactList() assert tuple(r.get_activations(fl)) == tuple()
def test_factlist_declare_raises_valueError(): """ declare raises valueerror if not ``Fact`` object providen """ from pyknow.factlist import FactList import pytest with pytest.raises(ValueError): FactList().declare("Foo")
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))
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
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))
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))
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))
def test_Rule_with_NOT_DEFINED(): from pyknow.rule import Rule, NOT from pyknow.factlist import FactList from pyknow.fact import Fact, InitialFact, L, W r = Rule(Fact(a=L(1)), NOT(Fact(b=W(True)))) fl = FactList() fl.declare(InitialFact()) fl.declare(Fact(a=L(1))) activations = r.get_activations(fl) assert len(list(activations)) == 1 fl.declare(Fact(b=L('SOMETHING'))) activations = r.get_activations(fl) assert len(list(activations)) == 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
def test_Rule_with_empty_Fact_matches_all_Facts(): from pyknow.rule import Rule from pyknow.factlist import FactList from pyknow.fact import Fact, L from pyknow.activation import Activation r = Rule(Fact()) fl = FactList() fl.declare(Fact(something=L(True))) fl.declare(Fact(something=L(False))) fl.declare(Fact(something=L(3))) activations = list(r.get_activations(fl)) assert len(activations) == 3 for i in range(3): assert Activation(None, (i, )) in activations
def test_FactList_cant_retract_twice(): from pyknow.factlist import FactList from pyknow.fact import Fact f0 = Fact() fl = FactList() idx = fl.declare(f0) fl.retract(idx) with pytest.raises(IndexError): fl.retract(idx)
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() st.update_agenda(a, first, []) assert a.activations == first st.update_agenda(a, second, first) assert a.activations == second
def test_Rule_with_empty_Fact_matches_all_Facts(): from pyknow.rule import Rule from pyknow.factlist import FactList from pyknow.fact import Fact from pyknow.activation import Activation r = Rule(Fact()) fl = FactList() fl.declare(Fact(something=True)) fl.declare(Fact(something=False)) fl.declare(Fact(something=3)) activations = r.get_activations(fl) assert len(activations) == 3 for i in range(3): assert Activation(r, (i, )) in activations
def test_Rule_simple_testce(): from pyknow.rule import Rule from pyknow.fact import Fact, T, L from pyknow.factlist import FactList r = Rule(Fact(a=T(lambda c, x: x.startswith('D')))) fl = FactList() fl.declare(Fact(a=L("David"))) fl.declare(Fact(a=L("Penelope"))) activations = list(r.get_activations(fl)) assert len(activations) == 1 assert {0} == set(activations[0].facts)
def test_factlist_changes(): """ Test factlist changes """ from pyknow.factlist import FactList from pyknow 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]
def test_Rule_nesting_issubclass(): """ This actually tests that nesting a Rule is permitted. Rule nesting can be useful for meta-stuff, and """ from pyknow.rule import Rule from pyknow.factlist import FactList from pyknow.fact import Fact, L r1 = Rule(Fact(a=L(1)), Fact(b=L(2)), Fact(c=L(3))) r2 = Rule(Fact(a=L(1)), Rule(Fact(b=L(2)), Fact(c=L(3)))) r3 = Rule(Fact(a=L(1)), Rule(Fact(b=L(2)), Rule(Fact(c=L(3))))) r4 = Rule(Rule(Fact(a=L(1))), Rule(Fact(b=L(2))), Rule(Fact(c=L(3)))) r5 = Rule(Rule(Fact(a=L(1)), Fact(b=L(2)), Fact(c=L(3)))) fl = FactList() fl.declare(Fact(a=L(1))) fl.declare(Fact(b=L(2))) fl.declare(Fact(c=L(3))) for r in (r1, r2, r3, r4, r5): activations = list(r.get_activations(fl)) assert len(activations) == 1 assert {0, 1, 2} == set(activations[0].facts)
def test_FactList_declare_reject_not_Fact_subclass(data): from pyknow.factlist import FactList fl = FactList() with pytest.raises(ValueError): fl.declare(data)
def __init__(self): self._facts = FactList() self.agenda = Agenda() self.strategy = self.__strategy__()
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_rule_with_NOT_testce(): from pyknow.rule import Rule, NOT from pyknow.factlist import FactList from pyknow.fact import Fact, InitialFact, L, T r = Rule(Fact(a=L(1)), NOT(Fact(b=T(lambda c, x: x.startswith('D'))))) fl = FactList() fl.declare(InitialFact()) fl.declare(Fact(a=L(1))) activations = r.get_activations(fl) assert len(list(activations)) == 1 fl.declare(Fact(b=L('David'))) activations = r.get_activations(fl) assert len(list(activations)) == 0 fl = FactList() fl.declare(InitialFact()) fl.declare(Fact(a=L(1))) fl.declare(Fact(b=L('Penelope'))) activations = r.get_activations(fl) assert len(list(activations)) == 1
def reset(self): self.agenda = Agenda() self._facts = FactList() self.declare(InitialFact())
def test_factlist_facts_idx_starts_zero(): """ Factlist idx starts at zero """ from pyknow.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:`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_Rule_multiple_criteria_generates_activation_with_matching_facts(): from pyknow.rule import Rule from pyknow.factlist import FactList from pyknow.fact import Fact r = Rule(Fact(a=1), Fact(b=2)) fl = FactList() fl.declare(Fact(a=1)) fl.declare(Fact(b=2)) activations = r.get_activations(fl) assert len(activations) == 1 assert {0, 1} == set(activations[0].facts)