diff --git a/core/builtin_concepts.py b/core/builtin_concepts.py index f493251..09fb804 100644 --- a/core/builtin_concepts.py +++ b/core/builtin_concepts.py @@ -24,12 +24,13 @@ class BuiltinConcepts(Enum): BEFORE_PARSING = 15 # activated before evaluation by the parsers PARSING = 16 # activated during the parsing. It contains the text to parse AFTER_PARSING = 17 # after parsing - BEFORE_EVALUATION = 18 # before evalution + BEFORE_EVALUATION = 18 # before evaluation EVALUATION = 19 # activated when the parsing process seems to be finished AFTER_EVALUATION = 20 # activated when the parsing process seems to be finished CONCEPT_ALREADY_DEFINED = 21 # when you try to add the same concept twice NOP = 22 # no operation concept. Does nothing - PROPERTY_EVAL_ERROR = 23 + PROPERTY_EVAL_ERROR = 23 # cannot evaluate a property of a concept + ENUMERATION = 24 # represents a list or a set """ diff --git a/core/concept.py b/core/concept.py index 80ae686..54449e4 100644 --- a/core/concept.py +++ b/core/concept.py @@ -24,8 +24,8 @@ class Concept: A concept is a the base object of our universe Everything is a concept """ - props_to_serialize = ("id", "is_builtin", "key", "name", "where", "pre", "post", "body", "desc", "obj") - props_for_digest = ("is_builtin", "key", "name", "where", "pre", "post", "body", "desc") + props_for_digest = ("is_builtin", "is_unique", "key", "name", "where", "pre", "post", "body", "desc") + props_to_serialize = ("id", "is_builtin", "is_unique", "key", "name", "where", "pre", "post", "body", "desc") concept_parts = set(item.value for item in ConceptParts) PROPERTY_PREFIX = "__var__" diff --git a/core/sheerka.py b/core/sheerka.py index cd22466..3665355 100644 --- a/core/sheerka.py +++ b/core/sheerka.py @@ -46,6 +46,9 @@ class Sheerka(Concept): self.parsers = [] self.evaluators = [] + self.evaluators_prefix = None + self.parsers_prefix = None + self.debug = debug def initialize(self, root_folder=None): @@ -327,7 +330,7 @@ class Sheerka(Concept): return self.ret(self.create_new_concept.__name__, False, ErrorConcept(error), error.args[0]) # add in cache for quick further reference - self.concepts_cache[concept.key] = concept + self.concepts_cache[concept.key] = self.sdp.get_safe(self.CONCEPTS_ENTRY, concept.key) # process the return in needed ret = self.ret(self.create_new_concept.__name__, True, self.new(BuiltinConcepts.NEW_CONCEPT, body=concept)) @@ -415,25 +418,12 @@ class Sheerka(Concept): concept_key != BuiltinConcepts.UNKNOWN_CONCEPT: return template - # manage singleton - if template.is_unique: - return template + if not isinstance(template, list): + return self._new_from_template(template, concept_key, **kwargs) - # otherwise, create another instance - concept = self.builtin_cache[concept_key]() if concept_key in self.builtin_cache else Concept() - concept.update_from(template) - - # update the properties - for k, v in kwargs.items(): - if k in concept.props: - concept.set_prop(k, v) - elif hasattr(concept, k): - setattr(concept, k, v) - else: - return self.new(BuiltinConcepts.UNKNOWN_PROPERTY, body=k, concept=concept) - - # TODO : add the concept to the list of known concepts (self.instances) - return concept + # if template is a list, it means that there a multiple concepts under the same key + concepts = [self._new_from_template(t, concept_key, **kwargs) for t in template] + return self.new(BuiltinConcepts.ENUMERATION, body=concepts) def ret(self, who, status, value, message=None, parents=None): """ @@ -473,6 +463,52 @@ class Sheerka(Concept): # for example, if a is a color, it will be found the entry 'All_Colors' return a.key == b_key + def get_evaluator_name(self, name): + if self.evaluators_prefix is None: + base_evaluator_class = core.utils.get_class("evaluators.BaseEvaluator.BaseEvaluator") + self.evaluators_prefix = base_evaluator_class.PREFIX + + return self.evaluators_prefix + name + + def get_parser_name(self, name): + if self.parsers_prefix is None: + base_parser_class = core.utils.get_class("parsers.BaseParser.BaseParser") + self.parsers_prefix = base_parser_class.PREFIX + + return self.parsers_prefix + name + + def concepts(self): + res = [] + lst = self.sdp.list(self.CONCEPTS_ENTRY) + for item in lst: + if isinstance(item, list): + res.extend(item) + else: + res.append(item) + + return sorted(res, key=lambda i: i.key) + + def _new_from_template(self, template, concept_key, **kwargs): + # manage singleton + if template.is_unique: + return template + + # otherwise, create another instance + concept = self.builtin_cache[concept_key]() if concept_key in self.builtin_cache else Concept() + concept.update_from(template) + + # update the properties + for k, v in kwargs.items(): + if k in concept.props: + concept.set_prop(k, v) + elif hasattr(concept, k): + setattr(concept, k, v) + else: + return self.new(BuiltinConcepts.UNKNOWN_PROPERTY, body=k, concept=concept) + + # TODO : add the concept to the list of known concepts (self.instances) + return concept + @staticmethod def get_builtins_classes_as_dict(): res = {} @@ -505,11 +541,11 @@ class ExecutionContext: """ To keep track of the execution of a request """ - who: object # who is asking - event_digest: str # what was the (original) trigger - sheerka: Sheerka # sheerka - desc: str = None # human description of what is going on - obj: Concept = None # what is the subject of the execution context (if known) + who: object # who is asking + event_digest: str # what was the (original) trigger + sheerka: Sheerka # sheerka + desc: str = None # human description of what is going on + obj: Concept = None # what is the subject of the execution context (if known) def push(self, who, desc=None, obj=None): return ExecutionContext(who, self.event_digest, self.sheerka, desc=desc, obj=obj) diff --git a/core/utils.py b/core/utils.py index 551f89b..882924f 100644 --- a/core/utils.py +++ b/core/utils.py @@ -132,3 +132,5 @@ def remove_from_list(lst, to_remove): lst.remove(item) return lst + + diff --git a/evaluators/AddConceptEvaluator.py b/evaluators/AddConceptEvaluator.py index 955d48d..4b288a5 100644 --- a/evaluators/AddConceptEvaluator.py +++ b/evaluators/AddConceptEvaluator.py @@ -13,9 +13,10 @@ class AddConceptEvaluator(OneReturnValueEvaluator): """ Used to add a new concept """ + NAME = "AddNewConcept" def __init__(self): - super().__init__("Add new Concept", 50) + super().__init__(self.NAME, 50) def matches(self, context, return_value): return return_value.status and \ diff --git a/evaluators/ConceptEvaluator.py b/evaluators/ConceptEvaluator.py index 76abea6..0f0079a 100644 --- a/evaluators/ConceptEvaluator.py +++ b/evaluators/ConceptEvaluator.py @@ -9,10 +9,11 @@ log = logging.getLogger(__name__) class ConceptEvaluator(OneReturnValueEvaluator): + NAME = "Concept" evaluation_steps = [BuiltinConcepts.EVALUATION, BuiltinConcepts.AFTER_EVALUATION] def __init__(self): - super().__init__("Concept Evaluator", 50) + super().__init__(self.NAME, 50) def matches(self, context, return_value): return return_value.status and \ diff --git a/evaluators/DuplicateConceptEvaluator.py b/evaluators/DuplicateConceptEvaluator.py index b2cb4c8..8570863 100644 --- a/evaluators/DuplicateConceptEvaluator.py +++ b/evaluators/DuplicateConceptEvaluator.py @@ -1,4 +1,5 @@ from core.builtin_concepts import BuiltinConcepts +from evaluators.AddConceptEvaluator import AddConceptEvaluator from evaluators.BaseEvaluator import AllReturnValuesEvaluator from parsers.BaseParser import BaseParser @@ -8,8 +9,10 @@ class DuplicateConceptEvaluator(AllReturnValuesEvaluator): Use to recognize when we tried to add the same concept twice """ + NAME = "DuplicateConcept" + def __init__(self): - super().__init__("Duplicate Concept Evaluator", 10) + super().__init__(self.NAME, 10) self.already_defined = None def matches(self, context, return_values): @@ -22,7 +25,7 @@ class DuplicateConceptEvaluator(AllReturnValuesEvaluator): if sheerka.isinstance(ret.value, BuiltinConcepts.AFTER_EVALUATION): if ret.status: parsing = True - elif ret.who == "Evaluators:Add new Concept": + elif ret.who == sheerka.get_evaluator_name(AddConceptEvaluator.NAME): if not ret.status and ret.value.body.args[0] == "Duplicate object.": add_concept_in_error = True self.already_defined = ret.value.body.obj diff --git a/evaluators/MutipleSameSuccessEvaluator.py b/evaluators/MutipleSameSuccessEvaluator.py index 4424e78..5cdab0a 100644 --- a/evaluators/MutipleSameSuccessEvaluator.py +++ b/evaluators/MutipleSameSuccessEvaluator.py @@ -14,8 +14,10 @@ class MultipleSameSuccessEvaluator(AllReturnValuesEvaluator): It has a low priority to let other evaluators try to resolve the errors """ + NAME = "MultipleSameSuccess" + def __init__(self): - super().__init__("Parsers Evaluator", 10) + super().__init__(self.NAME, 10) self.success = [] def matches(self, context, return_values): diff --git a/evaluators/OneSuccessEvaluator.py b/evaluators/OneSuccessEvaluator.py index b59290e..770e290 100644 --- a/evaluators/OneSuccessEvaluator.py +++ b/evaluators/OneSuccessEvaluator.py @@ -13,8 +13,10 @@ class OneSuccessEvaluator(AllReturnValuesEvaluator): It has a low priority to let other evaluators try to resolve the errors """ + NAME = "OneSuccess" + def __init__(self): - super().__init__("Parsers Evaluator", 10) + super().__init__(self.NAME, 10) self.successful_return_value = None def matches(self, context, return_values): diff --git a/evaluators/PythonEvaluator.py b/evaluators/PythonEvaluator.py index 1ec22a7..b857750 100644 --- a/evaluators/PythonEvaluator.py +++ b/evaluators/PythonEvaluator.py @@ -8,8 +8,11 @@ log = logging.getLogger(__name__) class PythonEvaluator(OneReturnValueEvaluator): + + NAME = "Python" + def __init__(self): - super().__init__("Python Evaluator", 50) + super().__init__(self.NAME, 50) def matches(self, context, return_value): return return_value.status and \ diff --git a/evaluators/TooManySuccessEvaluator.py b/evaluators/TooManySuccessEvaluator.py new file mode 100644 index 0000000..4de7c66 --- /dev/null +++ b/evaluators/TooManySuccessEvaluator.py @@ -0,0 +1,65 @@ +from core.builtin_concepts import BuiltinConcepts +from core.concept import Concept +from evaluators.BaseEvaluator import AllReturnValuesEvaluator, BaseEvaluator +import logging + +from parsers.BaseParser import BaseParser + +log = logging.getLogger(__name__) + + +class TooManySuccessEvaluator(AllReturnValuesEvaluator): + """ + Used to filter the responses + It has a low priority to let other evaluators try to resolve the errors + """ + + NAME = "TooManySuccess" + + def __init__(self): + super().__init__(self.NAME, 10) + self.success = [] + + def matches(self, context, return_values): + sheerka = context.sheerka + after_evaluation = False + nb_successful_evaluators = 0 + only_parsers_in_error = True + unlisted = False + + for ret in return_values: + + if sheerka.isinstance(ret.value, BuiltinConcepts.AFTER_EVALUATION): + if ret.status: + after_evaluation = True + + elif ret.who.startswith(BaseEvaluator.PREFIX): + if ret.status: + nb_successful_evaluators += 1 + self.success.append(ret) + elif ret.who.startswith(BaseParser.PREFIX): + if ret.status: + only_parsers_in_error = False + else: + unlisted = True + + return after_evaluation and nb_successful_evaluators > 1 and only_parsers_in_error and not unlisted + + def eval(self, context, return_values): + reference = self.get_value(self.success[0].value) + + for return_value in self.success[1:]: + actual = self.get_value(return_value.value) + if actual != reference: + sheerka = context.sheerka + too_many_success = sheerka.new(BuiltinConcepts.TOO_MANY_SUCCESS, obj=self.success) + return sheerka.ret(self.name, False, too_many_success, parents=return_values) + + return None + + @staticmethod + def get_value(obj): + if not isinstance(obj, Concept): + return obj + + return obj if obj.body is None else obj.body diff --git a/parsers/ExactConceptParser.py b/parsers/ExactConceptParser.py index bca9b72..08472fd 100644 --- a/parsers/ExactConceptParser.py +++ b/parsers/ExactConceptParser.py @@ -32,14 +32,16 @@ class ExactConceptParser(BaseParser): recognized = False for combination in self.combinations(words): - concept_key = " ".join(combination) - # Very important question to think about later - # Must we return a new instance or the existing one - # That will depend on the context - # Let's return a new one for now and see if it works - concept = sheerka.new(concept_key) - if not sheerka.isinstance(concept, BuiltinConcepts.UNKNOWN_CONCEPT): + concept_key = " ".join(combination) + result = sheerka.new(concept_key) + + if sheerka.isinstance(result, BuiltinConcepts.UNKNOWN_CONCEPT): + continue + + concepts = result.body if sheerka.isinstance(result, BuiltinConcepts.ENUMERATION) else [result] + + for concept in concepts: # update the properties if needed for i, token in enumerate(combination): if token.startswith(Concept.PROPERTY_PREFIX): diff --git a/sdp/sheerkaDataProvider.py b/sdp/sheerkaDataProvider.py index 24ef914..dcd54fe 100644 --- a/sdp/sheerkaDataProvider.py +++ b/sdp/sheerkaDataProvider.py @@ -576,7 +576,11 @@ class SheerkaDataProvider: if key is not None and key not in state.data[entry]: return None - return self.load_ref_if_needed(state.data[entry] if key is None else state.data[entry][key])[0] + item = state.data[entry] if key is None else state.data[entry][key] + if isinstance(item, list): + return [self.load_ref_if_needed(i)[0] for i in item] + + return self.load_ref_if_needed(item)[0] def exists(self, entry, key=None, digest=None): """ diff --git a/tests/test_sheerka.py b/tests/test_sheerka.py index 47f89d8..92ae12b 100644 --- a/tests/test_sheerka.py +++ b/tests/test_sheerka.py @@ -9,6 +9,7 @@ from core import utils from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept from core.concept import Concept, ConceptParts from core.sheerka import Sheerka, ExecutionContext +from evaluators.MutipleSameSuccessEvaluator import MultipleSameSuccessEvaluator from parsers.DefaultParser import DefaultParser from parsers.PythonParser import PythonParser from sdp.sheerkaDataProvider import SheerkaDataProvider, SheerkaDataProviderDuplicateKeyError @@ -158,6 +159,44 @@ def test_i_can_get_a_known_concept_when_not_in_cache(): assert loaded == concept +def test_i_can_get_list_of_concept_when_same_key_when_no_cache(): + sheerka = get_sheerka() + concept1 = get_default_concept() + concept2 = get_default_concept() + concept2.body = "a+b" + + res1 = sheerka.create_new_concept(get_context(sheerka), concept1) + res2 = sheerka.create_new_concept(get_context(sheerka), concept2) + + assert res1.value.body.key == res2.value.body.key # same key + + sheerka.concepts_cache = {} # reset the cache + + from_cache = sheerka.get(concept1.key) + assert len(from_cache) == 2 + assert from_cache[0] == concept1 + assert from_cache[1] == concept2 + + +def test_i_can_get_list_of_concept_when_same_key_when_cache(): + sheerka = get_sheerka() + concept1 = get_default_concept() + concept2 = get_default_concept() + concept2.body = "a+b" + + res1 = sheerka.create_new_concept(get_context(sheerka), concept1) + res2 = sheerka.create_new_concept(get_context(sheerka), concept2) + + assert res1.value.body.key == res2.value.body.key # same key + + # sheerka.concepts_cache = {} # Do not reset the cache + + from_cache = sheerka.get(concept1.key) + assert len(from_cache) == 2 + assert from_cache[0] == concept1 + assert from_cache[1] == concept2 + + def test_unknown_concept_is_return_when_the_concept_is_not_found(): sheerka = get_sheerka() @@ -538,6 +577,40 @@ def test_i_can_eval_duplicate_concepts_with_same_value(): assert len(res) == 1 assert res[0].status assert res[0].value, "hello foo" + assert res[0].who == sheerka.get_evaluator_name(MultipleSameSuccessEvaluator.NAME) + + +def test_i_cannot_manage_duplicate_concepts_when_the_values_are_different(): + sheerka = get_sheerka() + + sheerka.add_in_cache(Concept(name="hello a", body="'hello ' + a").set_prop("a")) + sheerka.add_in_cache(Concept(name="hello foo", body="'hello foo'")) + sheerka.add_in_cache(Concept(name="foo", body="'another value'")) + + res = sheerka.eval("hello foo") + assert len(res) == 1 + assert not res[0].status + assert sheerka.isinstance(res[0].value, BuiltinConcepts.TOO_MANY_SUCCESS) + + concepts = res[0].value.obj + assert len(concepts) == 2 + sorted_values = sorted(concepts, key=lambda x: x.value) + assert sorted_values[0].value == "hello another value" + assert sorted_values[1].value == "hello foo" + + +def test_i_can_manage_concepts_with_the_same_key_when_values_are_the_same(): + sheerka = get_sheerka() + context = get_context(sheerka) + + sheerka.create_new_concept(context, Concept(name="hello a", body="'hello ' + a").set_prop("a")) + sheerka.create_new_concept(context, Concept(name="hello b", body="'hello ' + b").set_prop("b")) + + res = sheerka.eval("hello 'foo'") + assert len(res) == 1 + assert res[0].status + assert res[0].value, "hello foo" + assert res[0].who == sheerka.get_evaluator_name(MultipleSameSuccessEvaluator.NAME) def get_sheerka(): diff --git a/tests/test_sheerkaDataProvider.py b/tests/test_sheerkaDataProvider.py index 0f9186e..b35c2a6 100644 --- a/tests/test_sheerkaDataProvider.py +++ b/tests/test_sheerkaDataProvider.py @@ -1316,3 +1316,20 @@ def test_i_can_save_and_load_object_ref_with_history(): state = sdp.load_state(sdp.get_snapshot()) assert state.data == {"entry": { "my_key": '##REF##:e6bf5b56428cfce0f08c94f2c3625dc3b3a8180d7229eaa9f8aa967fb16e5256'}} + + +def test_i_can_add_obj_with_same_key_and_get_them_back(): + sdp = SheerkaDataProvider(".sheerka") + obj1 = ObjDumpJson("key", "value1") + obj2 = ObjDumpJson("key", "value2") + sdp.serializer.register(ObjectSerializer(core.utils.get_full_qualified_name(obj1))) + + entry1, key1 = sdp.add(evt_digest, "entry", obj1, use_ref=True) + entry2, key2 = sdp.add(evt_digest, "entry", obj2, use_ref=True) + + loaded = sdp.get_safe(entry1, key1) + + assert len(loaded) == 2 + assert loaded[0] == obj1 + assert loaded[1] == obj2 +