import core.builtin_helpers from cache.Cache import Cache from cache.SetCache import SetCache from core.ast.nodes import python_to_concept from core.builtin_concepts import BuiltinConcepts from core.concept import Concept, ConceptParts, ensure_concept, DEFINITION_TYPE_BNF from core.sheerka.services.SheerkaModifyConcept import SheerkaModifyConcept from core.sheerka.services.sheerka_service import BaseService GROUP_PREFIX = 'All_' class SheerkaSetsManager(BaseService): NAME = "SetsManager" CONCEPTS_GROUPS_ENTRY = "SetsManager:Concepts_Groups" CONCEPTS_IN_GROUPS_ENTRY = "SetsManager:Concepts_In_Groups" # cache for get_set_elements() def __init__(self, sheerka): super().__init__(sheerka) self.sets = SetCache(default=lambda k: self.sheerka.sdp.get(self.CONCEPTS_GROUPS_ENTRY, k)) self.concepts_in_set = Cache() def initialize(self): self.sheerka.bind_service_method(self.set_isa, True) self.sheerka.bind_service_method(self.get_set_elements, True) # concepts are evaluated self.sheerka.bind_service_method(self.add_concept_to_set, True) self.sheerka.bind_service_method(self.isinset, False) self.sheerka.bind_service_method(self.isa, False) self.sheerka.bind_service_method(self.isaset, True) # concept is evaluated, need to change the code self.sheerka.cache_manager.register_cache(self.CONCEPTS_GROUPS_ENTRY, self.sets) self.sheerka.cache_manager.register_cache(self.CONCEPTS_IN_GROUPS_ENTRY, self.concepts_in_set, persist=False) def set_isa(self, context, concept, concept_set): """ Defines that concept a is b is another concept :param context: :param concept: :param concept_set: :return: """ context.log(f"Setting concept {concept} is a {concept_set}", who=self.NAME) ensure_concept(concept, concept_set) if BuiltinConcepts.ISA in concept.metadata.props and concept_set in concept.metadata.props[BuiltinConcepts.ISA]: return self.sheerka.ret( self.NAME, False, self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_IN_SET, body=concept, concept_set=concept_set)) # KSI 20200709 add the concept, not its 'id' or 'key' # It will allow conditions handling if concept set has its WHERE or PRE set to something concept.add_prop(BuiltinConcepts.ISA, concept_set) res = self.sheerka.modify_concept(context, concept) if not res.status: return res res = self.add_concept_to_set(context, concept, concept_set) return res def add_concept_to_set(self, context, concept, concept_set): """ Add an entry in sdp to tell that concept isa concept_set :param context: :param concept: :param concept_set: :return: """ context.log(f"Adding concept {concept} to set {concept_set}", who=self.NAME) ensure_concept(concept, concept_set) set_elements = self.sets.get(concept_set.id) if set_elements and concept.id in set_elements: return self.sheerka.ret( self.NAME, False, self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_IN_SET, body=concept, concept_set=concept_set)) self.sets.put(concept_set.id, concept.id) # invalidate the cache of what contains concept_set self.concepts_in_set.delete(concept_set.id) # update concept_set references self.sheerka.services[SheerkaModifyConcept.NAME].update_references(context, concept_set) return self.sheerka.ret(self.NAME, True, self.sheerka.new(BuiltinConcepts.SUCCESS)) def add_concepts_to_set(self, context, concepts, concept_set): """ Adding multiple concepts at the same time ******** THIS METHOD IS FOR TEST ONLY ************* As it is not optimized. It needs to be rewritten in case of production usage """ context.log(f"Adding concepts {concepts} to set {concept_set}", who=self.NAME) ensure_concept(concept_set) already_in_set = [] for concept in concepts: res = self.add_concept_to_set(context, concept, concept_set) if self.sheerka.isinstance(res.body, BuiltinConcepts.CONCEPT_ALREADY_IN_SET): already_in_set.append(res.body.body) if already_in_set: body = self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_IN_SET, body=already_in_set, concept_set=concept_set) else: body = self.sheerka.new(BuiltinConcepts.SUCCESS) self.concepts_in_set.delete(concept_set.id) return self.sheerka.ret(self.NAME, len(already_in_set) != len(concepts), body) def get_set_elements(self, context, concept): """ Concept is supposed to be a set Returns all elements if the set :param context: :param concept: :return: """ ensure_concept(concept) def _get_set_elements(sub_concept): if not self.isaset(context, sub_concept): return self.sheerka.new(BuiltinConcepts.NOT_A_SET, body=concept) # first, try to see if sub_concept has it's own group entry ids = self.sets.get(sub_concept.id) concepts = self._get_concepts(context, ids, True) # aggregate with en entries from its body sub_concept = core.builtin_helpers.ensure_evaluated(context, sub_concept) if not self.sheerka.is_success(sub_concept): return sub_concept if self.isaset(context, sub_concept.body): other_concepts = _get_set_elements(sub_concept.body) if not self.sheerka.is_success(other_concepts): return other_concepts concepts.extend(other_concepts) # apply the where clause if any if sub_concept.metadata.where: new_condition = self._validate_where_clause(sub_concept) if not new_condition: return self.sheerka.new(BuiltinConcepts.CONDITION_FAILED, body=sub_concept) # This methods sucks, but I don't have enough tools (like proper AST manipulation functions) # to do it properly now. It will be enhanced later globals_ = {"xx__concepts__xx": concepts, "sheerka": self.sheerka} locals_ = {} exec(new_condition, globals_, locals_) concepts = locals_["result"] return concepts # already in cache ? if res := self.concepts_in_set.get(concept.id): return res res = _get_set_elements(concept) # put in cache self.concepts_in_set.put(concept.id, res) return res def isinset(self, a, b): """ return true if the concept a is a b Will handle when the keyword isa will be implemented :param a: :param b: :return: """ ensure_concept(a, b) # TODO, first check the 'isa' property of a if not (a.id and b.id): return False group_elements = self.sets.get(b.id) return group_elements and a.id in group_elements def isa(self, a, b): ensure_concept(a, b) if BuiltinConcepts.ISA not in a.metadata.props: return False for c in a.metadata.props[BuiltinConcepts.ISA]: if c == b: return True if self.isa(c, b): return True return False def isaset(self, context, concept): """ True if exists All_ in sdp or if concept references to a concept that has all_ :param context: :param concept: :return: """ """""" if not (isinstance(concept, Concept) and concept.id): return False # KSI 20200629 # To resolve infinite recursion between group concepts and BNF concepts if concept.metadata.definition_type == DEFINITION_TYPE_BNF: return False # check if it has a group # TODO: use cache instead of directly requesting sdp if self.sets.get(concept.id): return True # it may be a concept that references a set concept = core.builtin_helpers.ensure_evaluated(context, concept) if not context.sheerka.is_success(concept): return False return self.isaset(context, concept.body) def _validate_where_clause(self, concept): python_parser_result = [r for r in concept.compiled[ConceptParts.WHERE] if r.who == "parsers.Python"] if not python_parser_result or not python_parser_result[0].status: return None ast_ = python_parser_result[0].body.body.ast_ ast_as_concepts = python_to_concept(ast_) names = core.builtin_helpers.get_names(self.sheerka, ast_as_concepts) if len(names) != 1 or names[0] != concept.metadata.body: return None condition = concept.metadata.where.replace(concept.metadata.body, "sheerka.objvalue(x)") expression = f""" result=[] for x in xx__concepts__xx: try: if {condition}: result.append(x) except Exception: pass """ return expression def _get_concepts(self, context, ids, evaluate): """ Gets concepts from a list of concepts ids :param ids: :param evaluate: if True, all the elements are evaluated before returned :return: """ if not ids: return [] if not evaluate: return [self.sheerka.get_by_id(element_id) for element_id in ids] result = [] with context.push(BuiltinConcepts.EVALUATE_CONCEPT, {"ids": ids}, desc=f"Evaluating concepts of a set") as sub_context: sub_context.add_inputs(ids=ids) sub_context.protected_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED) errors = [] for element_id in ids: concept = self.sheerka.get_by_id(element_id) if len(concept.metadata.variables) == 0: # The concepts are directly taken from Sheerka.get_by_id, so variable cannot be filled # It's the reason why we only evaluate concept with no variable evaluated = self.sheerka.evaluate_concept(sub_context, concept) if context.sheerka.is_success(evaluated): result.append(evaluated) else: errors.append(evaluated) else: result.append(concept) sub_context.add_values(return_value=result) sub_context.add_values(errors=errors) return result