diff --git a/src/core/sheerka/services/SheerkaRuleManager.py b/src/core/sheerka/services/SheerkaRuleManager.py index bfcb332..21642f7 100644 --- a/src/core/sheerka/services/SheerkaRuleManager.py +++ b/src/core/sheerka/services/SheerkaRuleManager.py @@ -6,7 +6,7 @@ from typing import Union, Set, List from cache.Cache import Cache from cache.ListIfNeededCache import ListIfNeededCache from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept -from core.builtin_helpers import is_a_question, ensure_evaluated, expect_one +from core.builtin_helpers import is_a_question, ensure_evaluated, expect_one, evaluate from core.concept import Concept from core.global_symbols import EVENT_RULE_PRECEDENCE_MODIFIED, RULE_COMPARISON_CONTEXT, NotFound, ErrorObj, \ EVENT_RULE_CREATED, EVENT_RULE_DELETED @@ -14,7 +14,7 @@ from core.rule import Rule, ACTION_TYPE_PRINT from core.sheerka.Sheerka import RECOGNIZED_BY_NAME, RECOGNIZED_BY_ID from core.sheerka.services.sheerka_service import BaseService, FailedToCompileError from core.tokenizer import Keywords, TokenKind, Token, IterParser -from core.utils import index_tokens, COLORS, get_text_from_tokens, unstr_concept +from core.utils import index_tokens, COLORS, get_text_from_tokens from evaluators.ConceptEvaluator import ConceptEvaluator from evaluators.PythonEvaluator import PythonEvaluator, Expando from parsers.BaseExpressionParser import AndNode, ExpressionVisitor, VariableNode, ComparisonNode, FunctionNode @@ -1134,22 +1134,34 @@ class ReteConditionExprVisitor(ExpressionVisitor): self.variables[target] = var_name return var_name - def init_or_get_variable_from_name(self, node, conditions): + def init_or_get_variable_from_name(self, node: VariableNode, conditions): + + if node.attributes: + left = node.attributes[:-1] + right = [node.attributes[-1]] + + while left: + var_name = node.name + "." + ".".join(left) + if var_name in self.variables: + return V(self.variables[var_name]), ".".join(right) + + right.insert(0, left.pop()) + if node.name not in self.variables: var_name = self.add_variable(node.name) conditions.append(Condition(V(var_name), "__name__", node.name)) - return V(self.variables[node.name]) + return V(self.variables[node.name]), node.attributes_str - def init_or_get_variable_from_attr(self, node, conditions): + def init_or_get_variable_from_attr(self, node: VariableNode, conditions): path = f"{node.name}.{node.attributes_str}" if path in self.variables: return self.variables[path] - root = self.init_or_get_variable_from_name(node, conditions) + root, attr = self.init_or_get_variable_from_name(node, conditions) var_name = self.add_variable(path) variable = V(var_name) - conditions.append(Condition(root, node.attributes_str, variable)) + conditions.append(Condition(root, attr, variable)) return variable def get_conditions(self, expr_node): @@ -1161,9 +1173,9 @@ class ReteConditionExprVisitor(ExpressionVisitor): def visit_VariableNode(self, expr_node): conditions = [] - var_name = self.init_or_get_variable_from_name(expr_node, conditions) + var_name, attr = self.init_or_get_variable_from_name(expr_node, conditions) if expr_node.attributes_str is not None: - conditions.append(Condition(var_name, expr_node.attributes_str, True)) + conditions.append(Condition(var_name, attr, True)) return conditions def visit_AndNode(self, expr_node: AndNode): @@ -1176,29 +1188,74 @@ class ReteConditionExprVisitor(ExpressionVisitor): def visit_ComparisonNode(self, expr_node: ComparisonNode): if isinstance(expr_node.left, VariableNode): conditions = [] - left = self.init_or_get_variable_from_name(expr_node.left, conditions) - attr = expr_node.left.attributes_str or "__self__" - right = eval(get_text_from_tokens(expr_node.right.tokens)) - conditions.append(Condition(left, attr, right)) + left, attr = self.init_or_get_variable_from_name(expr_node.left, conditions) + + res = evaluate(self.context, + expr_node.right.get_source(), + evaluators="all", # TODO: all is too much + desc=None, + eval_body=False, + eval_where=False, + is_question=False, + expect_success=False, + stm=None) + res = expect_one(self.context, res) + if not res.status: + return FailedToCompileError([f"Cannot recognize '{expr_node.right.get_source()}'"]) + + value = res.value + if (isinstance(value, Expando) and value.get_name() == "sheerka" or + isinstance(value, Concept) and value.id == self.context.sheerka.id): + conditions.append(Condition(left, attr, "__sheerka__")) + else: + conditions.append(Condition(left, attr, res.value)) return conditions else: raise FailedToCompileError([expr_node]) def visit_FunctionNode(self, expr_node: FunctionNode): if expr_node.first.value == "recognize(": - return self.function_recognize(expr_node.parameters[0].value, expr_node.parameters[1].value) + return self.function_recognize_concept(expr_node.parameters[0].value, + expr_node.parameters[1].value, + [p.value for p in expr_node.parameters[2:]]) - def function_recognize(self, source, target): + def function_recognize_concept(self, variable_path, concept_to_recognize, parameters): + """ + Creates Rete conditions to recognize a concept + :param variable_path: variable holding the information + :param concept_to_recognize: concept to recognize + :param parameters: concept variables values + :return: + """ + + concept_as_str = concept_to_recognize.get_source() + if not concept_as_str: + return FailedToCompileError([f"Missing concept in for {variable_path}"]) + + res = evaluate(self.context, + concept_as_str, + evaluators="all", # TODO: all is too much + desc=None, + eval_body=False, + eval_where=False, + is_question=False, + expect_success=False, + stm=None) + res = expect_one(self.context, res) + + if not res.status: + return FailedToCompileError([f"Unknown concept {concept_as_str}"]) + + concept_found = res.body conditions = [] - body_var = self.init_or_get_variable_from_attr(source, conditions) - conditions.append(Condition(body_var, "__is_concept__", True)) - if isinstance(target, VariableNode): - conditions.append(Condition(body_var, "name", target.name)) + variable = self.init_or_get_variable_from_attr(variable_path, conditions) + conditions.append(Condition(variable, "__is_concept__", True)) + + if concept_found.get_hint(BuiltinConcepts.RECOGNIZED_BY) == RECOGNIZED_BY_NAME: + conditions.append(Condition(variable, "name", concept_found.name)) + elif concept_found.get_hint(BuiltinConcepts.RECOGNIZED_BY) == RECOGNIZED_BY_ID: + conditions.append(Condition(variable, "id", concept_found.id)) else: - concept_key, concept_id = unstr_concept(target.value) - if concept_id: - conditions.append(Condition(body_var, "id", concept_id)) - elif concept_key: - conditions.append(Condition(body_var, "name", concept_key)) + conditions.append(Condition(variable, "key", concept_found.key)) return conditions diff --git a/src/parsers/BaseExpressionParser.py b/src/parsers/BaseExpressionParser.py index be61fe1..394b0b1 100644 --- a/src/parsers/BaseExpressionParser.py +++ b/src/parsers/BaseExpressionParser.py @@ -4,7 +4,7 @@ from typing import List, Tuple, Union from core.builtin_concepts_ids import BuiltinConcepts from core.sheerka.services.SheerkaExecute import ParserInput from core.tokenizer import Token, TokenKind, Tokenizer, LexerError -from core.utils import tokens_are_matching +from core.utils import tokens_are_matching, get_text_from_tokens from parsers.BaseNodeParser import UnrecognizedTokensNode from parsers.BaseParser import Node, ParsingError, BaseParser, ErrorSink, UnexpectedTokenParsingError @@ -33,7 +33,7 @@ class ParenthesisMismatchError(ParsingError): token: Token -@dataclass +@dataclass(init=False) class ExprNode(Node): """ Base ExprNode @@ -43,6 +43,12 @@ class ExprNode(Node): end: int # index of the last token tokens: List[Token] + def __init__(self, start: int, end: int, tokens: List[Token]): + self.start = start + self.end = end + self.tokens = tokens + self.source = None + def eval(self, obj): return True @@ -61,6 +67,12 @@ class ExprNode(Node): def __hash__(self): return hash((self.start, self.end)) + def get_source(self): + if self.source is None: + self.source = get_text_from_tokens(self.tokens) + + return self.source + class NameExprNode(ExprNode): def __init__(self, start, end, tokens): diff --git a/src/sheerkarete/network.py b/src/sheerkarete/network.py index d398873..2bb39fe 100644 --- a/src/sheerkarete/network.py +++ b/src/sheerkarete/network.py @@ -3,10 +3,13 @@ from __future__ import annotations from itertools import product from typing import TYPE_CHECKING, Generator, Union +from core.builtin_concepts_ids import BuiltinConcepts from core.concept import Concept from core.global_symbols import NotInit from core.rule import Rule, ACTION_TYPE_PRINT from core.utils import as_bag +from evaluators.PythonEvaluator import Expando +from sheerkapickle.utils import is_primitive from sheerkarete.alpha import AlphaMemory from sheerkarete.beta import ReteNode, BetaMemory from sheerkarete.bind_node import BindNode @@ -413,7 +416,13 @@ class ReteNetwork: else: try: value = getattr(obj, attribute) - inner_add_vme(name, fact_id, attribute, value) + if (isinstance(value, Concept) and value.key == BuiltinConcepts.SHEERKA or + isinstance(value, Expando) and value.get_name() == "sheerka"): + value = "__sheerka__" + if is_primitive(value): + self.add_wme(WME(fact_id, attribute, value)) + else: + inner_add_vme(name, fact_id, attribute, value) except AttributeError: pass diff --git a/tests/core/test_SheerkaRuleManager.py b/tests/core/test_SheerkaRuleManager.py index e9a798e..3875db9 100644 --- a/tests/core/test_SheerkaRuleManager.py +++ b/tests/core/test_SheerkaRuleManager.py @@ -1085,40 +1085,10 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ "#__x_01__|__is_concept__|True", "#__x_01__|name|'greetings'"] ), - ( - "recognize by name and variable sheerka", - "recognize(__ret.body, greetings, a=sheerka)", - "sheerka", - ["#__x_00__|__name__|'__ret'", - "#__x_00__|body|#__x_01__", - "#__x_01__|__is_concept__|True", - "#__x_01__|name|'greetings'", - "#__x_01__|a|'__sheerka__'"] - ), - ( - "recognize by name and str variable", - "recognize(__ret.body, greetings, a='my friend')", - "'my friend'", - ["#__x_00__|__name__|'__ret'", - "#__x_00__|body|#__x_01__", - "#__x_01__|__is_concept__|True", - "#__x_01__|name|'greetings'", - "#__x_01__|a|'my friend'"] - ), - ( - "recognize by name and concept variable", - "recognize(__ret.body, greetings, a=foo)", - "foo", - ["#__x_00__|__name__|'__ret'", - "#__x_00__|body|#__x_01__", - "#__x_01__|__is_concept__|True", - "#__x_01__|name|'greetings'", - "#__x_01__|a.name|'foo'"] - ), ( "recognize by name and add other conditions (str)", "recognize(__ret.body, greetings) and __ret.body.a == 'my friend'", - "foo", + "my friend", ["#__x_00__|__name__|'__ret'", "#__x_00__|body|#__x_01__", "#__x_01__|__is_concept__|True", @@ -1128,12 +1098,12 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ ( "recognize by name and add other conditions (sheerka)", "recognize(__ret.body, greetings) and __ret.body.a == sheerka", - "foo", + "sheerka", ["#__x_00__|__name__|'__ret'", "#__x_00__|body|#__x_01__", "#__x_01__|__is_concept__|True", "#__x_01__|name|'greetings'", - "#__x_01__|a|'kodjo'"] + "#__x_01__|a|'__sheerka__'"] ), ( "recognize by name and add other conditions (concept)", @@ -1143,8 +1113,21 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ "#__x_00__|body|#__x_01__", "#__x_01__|__is_concept__|True", "#__x_01__|name|'greetings'", - "#__x_01__|a|'kodjo'"] + "#__x_01__|a|#__x_02__", + "#__x_02__|__is_concept__|True", + "#__x_02__|name|'foo'"] ), + ( + "recognize by instance", + "recognize(__ret.body, hello sheerka)", + "foo", + ["#__x_00__|__name__|'__ret'", + "#__x_00__|body|#__x_01__", + "#__x_01__|__is_concept__|True", + "#__x_01__|key|'hello __var__0'", + "#__x_01__|a|__sheerka__"] + ), + ]) def test_i_can_get_rete_conditions_from_recognized(self, test_name, expression, variable_name, expected_as_str): sheerka, context, greetings, foo = self.init_test().with_concepts( @@ -1173,7 +1156,7 @@ isinstance(var, Concept) and var.key == 'hello __var__0'""" + \ rule.rete_disjunctions = conditions network.add_rule(rule) - variable = foo if variable_name == "foo" else variable_name + variable = foo if variable_name == "foo" else sheerka if variable_name == "sheerka" else variable_name to_recognize = sheerka.new_from_template(greetings, greetings.key, a=variable) network.add_obj("__ret", ReturnValueConcept("Test", True, to_recognize)) matches = list(network.matches) diff --git a/tests/sheerkarete/test_network.py b/tests/sheerkarete/test_network.py index 88103b2..c41eb65 100644 --- a/tests/sheerkarete/test_network.py +++ b/tests/sheerkarete/test_network.py @@ -134,7 +134,7 @@ class TestReteNetwork(TestUsingMemoryBasedSheerka): def test_adding_obj_when_requested_attribute_is_not_found(self): """ - When a rule with attribute constraint, we only add the requested attributes + There is no error when an attribute does not exits """ network = ReteNetwork() ret = ReturnValueConcept("test", True, "value") @@ -218,6 +218,35 @@ class TestReteNetwork(TestUsingMemoryBasedSheerka): assert matches[0].pnode.rules == [rule] assert network.facts == {'f-00000': ret} + def test_i_can_add_obj_and_match_obj_when_value_is_sheerka(self): + sheerka, context, greetings = self.init_concepts( + Concept("greetings", definition="hello a", definition_type=DEFINITION_TYPE_DEF).def_var("a") + ) + network = ReteNetwork() + rule = RuleForTestingRete(AndConditions([Condition(V("ret"), "__name__", "__ret"), + Condition(V("ret"), "body", V("body")), + Condition(V("body"), "name", "greetings"), + Condition(V("body"), "a", "__sheerka__"), + ])) + network.add_rule(rule) + + hello_concept = sheerka.new(greetings, a=sheerka) + ret = ReturnValueConcept("test", True, hello_concept) + network.add_obj("__ret", ret) + + assert network.working_memory == { + WME("f-00000", "__name__", "__ret"), + WME("f-00000", "body", "f-00000.body"), + WME("f-00000.body", "name", "greetings"), + WME("f-00000.body", "a", "__sheerka__"), + } + + # sanity check that the WME produced match the condition + matches = list(network.matches) + assert len(matches) == 1 + assert matches[0].pnode.rules == [rule] + assert network.facts == {'f-00000': ret} + def test_i_can_distinguish_objects_with_different_value(self): network = ReteNetwork() rule = RuleForTestingRete(AndConditions([