diff --git a/src/core/rule.py b/src/core/rule.py index cb3a8e9..90409ae 100644 --- a/src/core/rule.py +++ b/src/core/rule.py @@ -31,7 +31,8 @@ class Rule: rule_id=None, is_enabled=None): self.metadata = RuleMetadata(action_type, name, predicate, action, id=rule_id, is_enabled=is_enabled) - self.compiled_predicates = None + self.compiled_predicates = None # old version (to remove when possible) + self.compiled_conditions = None # new version self.compiled_action = None from core.sheerka.services.SheerkaComparisonManager import SheerkaComparisonManager self.priority = priority if priority is not None else SheerkaComparisonManager.DEFAULT_COMPARISON_VALUE diff --git a/src/core/sheerka/services/SheerkaEvaluateRules.py b/src/core/sheerka/services/SheerkaEvaluateRules.py index b1b78fd..e9dfba3 100644 --- a/src/core/sheerka/services/SheerkaEvaluateRules.py +++ b/src/core/sheerka/services/SheerkaEvaluateRules.py @@ -85,7 +85,7 @@ class SheerkaEvaluateRules(BaseService): results.setdefault(LOW_PRIORITY_RULES, []).append(rule) continue - res = self.evaluate_rule(sub_context, rule, bag) + res = self.evaluate_rule_old(sub_context, rule, bag) ok = res.status and self.sheerka.is_success(self.sheerka.objvalue(res)) results.setdefault(ok, []).append(rule) if ok and success_priority is None: @@ -96,9 +96,9 @@ class SheerkaEvaluateRules(BaseService): sub_context.add_values(rules_result=results) return results - def evaluate_rule(self, context, rule, bag): + def evaluate_rule_old(self, context, rule, bag): """ - Evaluate all the predicate + Evaluate the conditions :param context: :param rule: :param bag: @@ -132,6 +132,47 @@ class SheerkaEvaluateRules(BaseService): return expect_one(context, results) + def evaluate_rule(self, context, rule, bag): + """ + Evaluate the conditions + :param context: + :param rule: + :param bag: + :return: + """ + + bag_variables = set(bag.keys()) + + results = [] + for compiled_condition in rule.compiled_conditions: + + if compiled_condition.variables.intersection(bag_variables) != compiled_condition.variables: + continue + + if compiled_condition.return_value is None: + # We only want to test the existence of a data + results.append(context.sheerka.ret(self.NAME, True, True)) + + else: + + # do not forget to reset the 'is_evaluated' in the case of a concept + if compiled_condition.evaluator_type == ConceptEvaluator.NAME: + compiled_condition.concept.get_metadata().is_evaluated = False + + evaluator = self.evaluators_by_name[compiled_condition.evaluator] + res = evaluator.eval(context, compiled_condition.return_value) + if res.status and isinstance(res.body, bool) and res.body: + # one successful value found. No need to look any further + results = [res] + break + else: + results.append(res) + + debugger = context.get_debugger(SheerkaEvaluateRules.NAME, "evaluate_rule", new_debug_id=False) + debugger.debug_rule(rule, results) + + return expect_one(context, results) + def remove_from_rete_memory(self, lst): if lst is None: return diff --git a/src/core/sheerka/services/SheerkaRuleManager.py b/src/core/sheerka/services/SheerkaRuleManager.py index 9148652..ec2927e 100644 --- a/src/core/sheerka/services/SheerkaRuleManager.py +++ b/src/core/sheerka/services/SheerkaRuleManager.py @@ -628,6 +628,18 @@ class RuleCompiledPredicate: variables: Set[str] = None # TODO: set of required variables +@dataclass() +class CompiledCondition: + """ + The 'when' expression is parsed to have a ReturnValueConcept or a Concept that can then be evaluated + Depending on the evaluator, the 'predicate' attribute or the 'concept' attribute will be used + """ + evaluator_type: Union[str, None] # PythonEvaluator.NAME | ConceptEvaluator.NAME + return_value: Union[ReturnValueConcept, None] # compiled source as ReturnValue + variables: Set[str] + concept: Union[Concept, None] = None # compiled source as concept + + @dataclass class CompiledWhenResult: """ @@ -1298,4 +1310,18 @@ class ReteConditionExprVisitor(ExpressionVisitor): class PythonConditionExprVisitor(ExpressionVisitor): - pass + def __init__(self, context): + self.context = context + self.var_counter = 0 + self.variables = set() + + def get_conditions(self, expr_node): + self.var_counter = 0 + self.variables.clear() + + condition = self.visit(expr_node) + return [condition] + + def visit_VariableNode(self, expr_node: VariableNode): + # no evaluator to call, simply check that the variable is in the bag + return CompiledCondition(None, None, {expr_node.name}) diff --git a/tests/core/test_SheerkaRuleManager.py b/tests/core/test_SheerkaRuleManager.py index 7061b3b..e1d1b22 100644 --- a/tests/core/test_SheerkaRuleManager.py +++ b/tests/core/test_SheerkaRuleManager.py @@ -7,12 +7,14 @@ from core.concept import Concept, DEFINITION_TYPE_DEF, DoNotResolve from core.global_symbols import RULE_COMPARISON_CONTEXT, NotFound, EVENT_RULE_DELETED from core.rule import Rule, ACTION_TYPE_PRINT, ACTION_TYPE_EXEC from core.sheerka.Sheerka import RECOGNIZED_BY_ID, RECOGNIZED_BY_NAME +from core.sheerka.services.SheerkaEvaluateRules import SheerkaEvaluateRules from core.sheerka.services.SheerkaExecute import ParserInput from core.sheerka.services.SheerkaRuleManager import SheerkaRuleManager, FormatRuleActionParser, \ FormatAstRawText, FormatAstVariable, FormatAstSequence, FormatAstFunction, \ FormatRuleSyntaxError, FormatAstList, UnexpectedEof, FormatAstColor, RuleCompiledPredicate, FormatAstDict, \ FormatAstMulti, \ - PythonCodeEmitter, NoConditionFound, FormatAstNode, ReteConditionExprVisitor + PythonCodeEmitter, NoConditionFound, FormatAstNode, ReteConditionExprVisitor, PythonConditionExprVisitor, \ + CompiledCondition from core.sheerka.services.sheerka_service import FailedToCompileError from core.tokenizer import Token, TokenKind from parsers.BaseNodeParser import SourceCodeWithConceptNode, SourceCodeNode @@ -1240,6 +1242,7 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ matches = list(network.matches) assert len(matches) == 1 + @pytest.mark.skip("I am not sure yet of what I want to get") @pytest.mark.parametrize("expression, expected_as_str", [ ( "eval(__ret.body, 'foo' starts with 'f')", @@ -1287,6 +1290,74 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ matches = list(network.matches) assert len(matches) == 1 + def test_i_can_get_compiled_conditions_when_testing_data_existence(self): + sheerka, context = self.init_test().unpack() + expression = "__ret" + + parser = ExpressionParser() + error_sink = ErrorSink() + parser_input = ParserInput(expression) + parser.reset_parser_input(parser_input, error_sink) + parsed = parser.parse_input(context, parser_input, error_sink) + + visitor = PythonConditionExprVisitor(context) + conditions = visitor.get_conditions(parsed) + + assert len(conditions) == 1 + assert isinstance(conditions[0], CompiledCondition) + assert conditions[0].evaluator_type is None + assert conditions[0].return_value is None + assert conditions[0].concept is None + assert conditions[0].variables == {"__ret"} + + # check against SheerkaEvaluateRules + evaluate_rules_service = sheerka.services[SheerkaEvaluateRules.NAME] + bag = {"__ret": ReturnValueConcept("Test", True, None)} + rule = Rule(name="test_i_can_get_compiled_conditions", predicate=expression) + rule.compiled_conditions = conditions + res = evaluate_rules_service.evaluate_rule(context, rule, bag) + assert res.status + assert self.sheerka.is_success(self.sheerka.objvalue(res)) + + @pytest.mark.parametrize("expression", [ + "__ret", + "__ret.status == True", + "__ret.status", + "__ret and __ret.status", + ]) + def test_i_can_get_compiled_conditions(self, expression): + sheerka, context = self.init_test().unpack() + + parser = ExpressionParser() + error_sink = ErrorSink() + parser_input = ParserInput(expression) + parser.reset_parser_input(parser_input, error_sink) + parsed = parser.parse_input(context, parser_input, error_sink) + + visitor = PythonConditionExprVisitor(context) + conditions = visitor.get_conditions(parsed) + + ast_ = ast.parse(expression, "", 'eval') + expected_python_node = PythonNode(expression, ast_) + + assert len(conditions) == 1 + assert isinstance(conditions[0], CompiledCondition) + assert conditions[0].evaluator_type == PYTHON_EVALUATOR_NAME + assert sheerka.isinstance(conditions[0].return_value, BuiltinConcepts.RETURN_VALUE) + assert sheerka.objvalue(conditions[0].return_value) == expected_python_node + assert conditions[0].concept is None + assert conditions[0].variables == {"__ret"} + + # check against SheerkaEvaluateRules + evaluate_rules_service = sheerka.services[SheerkaEvaluateRules.NAME] + bag = {"__ret": ReturnValueConcept("Test", True, None)} + rule = Rule(name="test_i_can_get_compiled_conditions", predicate=expression) + rule.compiled_conditions = conditions + res = evaluate_rules_service.evaluate_rule(context, rule, bag) + assert res.status + assert self.sheerka.is_success(self.sheerka.objvalue(res)) + + class TestSheerkaRuleManagerUsingFileBasedSheerka(TestUsingFileBasedSheerka): def test_rules_are_initialized_at_startup(self): sheerka, context, *rules = self.init_test().with_rules(