import logging import pprint import time from core.builtin_concepts import BuiltinConcepts, ParserResultConcept from core.concept import Concept, get_concept_attrs from core.global_symbols import EVENT_CONTEXT_DISPOSED, NO_MATCH from core.sheerka.services.SheerkaMemory import SheerkaMemory from core.utils import CONSOLE_COLORS_MAP as CCM, CONSOLE_COLUMNS from sdp.sheerkaDataProvider import Event pp = pprint.PrettyPrinter(indent=2, width=CONSOLE_COLUMNS) DEBUG_TAB_SIZE = 4 PROPERTIES_TO_SERIALIZE = ("_id", "_children", "_start", "_stop", "who", "action", "action_context", "desc", "inputs", "values", "obj", "concepts") class ExecutionContext: """ To keep track of the execution of a request """ ids = {} @staticmethod def get_id(event_digest): if event_digest in ExecutionContext.ids: ExecutionContext.ids[event_digest] += 1 else: ExecutionContext.ids[event_digest] = 0 return ExecutionContext.ids[event_digest] def __init__(self, who, event: Event, sheerka, action: BuiltinConcepts, action_context, desc: str = None, logger=None, global_hints=None, errors=None, obj=None, concepts=None): self._id = ExecutionContext.get_id(event.get_digest()) if event else None self._parent = None self._children = [] self._start = 0 # when the execution starts (to measure elapsed time) self._stop = 0 # when the execution stops (to measure elapses time) self._logger = logger self._format_instructions = None # how to print the execution context self._push = None self.who = who # who is asking self.event = event # what was the (original) trigger self.sheerka = sheerka # sheerka self.action = action self.action_context = action_context self.desc = desc # human description of what is going on self.preprocess_parsers = None self.preprocess_evaluators = None self.preprocess = None self.stm = False # True if the context has short term memory entries self.private_hints = set() self.protected_hints = set() self.global_hints = set() if global_hints is None else global_hints self.errors = [] if errors is None else errors # error are global self.inputs = {} # what were the parameters of the execution context self.values = {} # what was produced by the execution context self.obj = obj self.concepts = concepts @property def elapsed(self): if self._start == 0: return 0 return (self._stop if self._stop > 0 else time.time_ns()) - self._start @property def elapsed_str(self): nano_sec = self.elapsed dt = nano_sec / 1e6 return f"{dt} ms" if dt < 1000 else f"{dt / 1000} s" @property def logger(self): return self._logger @property def id(self): return self._id @property def achildren(self): """ I prefixed with an 'a' to make it appear on the top when debugging :return: """ return self._children def __enter__(self): self._start = time.time_ns() # self.log_new() return self def __exit__(self, exc_type, exc_val, exc_tb): if self._push: return if self.stm: self.sheerka.publish(self, EVENT_CONTEXT_DISPOSED) self._stop = time.time_ns() def __repr__(self): msg = f"ExecutionContext(who={self.who}, id={self._id}, action={self.action}, context={self.action_context}" if self.desc: msg += f", desc='{self.desc}'" msg += ")" return msg def __eq__(self, other): if id(self) == id(other): return True if not isinstance(other, ExecutionContext): return False for prop in PROPERTIES_TO_SERIALIZE: if prop in ("who", "action", "action_context"): value = str(getattr(self, prop)) other_value = str(getattr(other, prop)) else: value = getattr(self, prop) other_value = getattr(other, prop) if value != other_value: return False return True def push(self, action: BuiltinConcepts, action_context, who=None, desc=None, logger=None, obj=None, concepts=None): if self._push: return self._push who = who or self.who logger = logger or self._logger new = ExecutionContext( who, self.event, self.sheerka, action, action_context, desc, logger, self.global_hints, self.errors, obj or self.obj, concepts or self.concepts) new._parent = self new.preprocess = self.preprocess new.preprocess_parsers = self.preprocess_parsers new.preprocess_evaluators = self.preprocess_evaluators new.protected_hints.update(self.protected_hints) self._children.append(new) return new def deactivate_push(self): self._push = self.push(BuiltinConcepts.NOP, None) self._push._push = self._push if self.stm: bag = self.sheerka.services[SheerkaMemory.NAME].get_all_short_term_memory(self) self.sheerka.add_many_to_short_term_memory(self._push, bag) def activate_push(self): if self._push: if self._push.stm: self.sheerka.publish(self._push, EVENT_CONTEXT_DISPOSED) self._push._stop = time.time_ns() self._push = None def add_preprocess(self, name, **kwargs): """ PreProcess item are used during the parsing and the evaluation of the ReturnValueConcept Using them, you can twitch the behaviour of parser and evaluator (you can disable them for instance) example : context.add_preprocess(BaseEvaluator.get_name("priority15"), enabled=False) context.add_preprocess(BaseEvaluator.get_name("all_priority15"), priority=99) :param name: :param kwargs: :return: """ preprocess = self.sheerka.new(BuiltinConcepts.EVALUATOR_PRE_PROCESS) preprocess.set_value("preprocess_name", name) for k, v in kwargs.items(): preprocess.set_value(k, v) if not self.preprocess: self.preprocess = [] self.preprocess.append(preprocess) return self def add_inputs(self, **kwargs): if self._push: return self.inputs.update(kwargs) return self def add_values(self, **kwargs): if self._push: return self.values.update(kwargs) return self def add_to_short_term_memory(self, key, concept): """ Add a concept to the short term memory (relative to the current execution context) :param key: :param concept: :return: """ self.sheerka.add_to_short_term_memory(self, key, concept) def clear_short_term_memory(self): self.sheerka.clear_short_term_memory(self) def get_from_short_term_memory(self, key): """ :param key: :return: """ return self.sheerka.get_from_short_term_memory(self, key) def get_concept(self, key): # search in obj if isinstance(self.obj, Concept): if self.obj.key == key: return self.obj if key in get_concept_attrs(self.obj): value = self.obj.get_value(key) if isinstance(value, Concept): return value # search in concepts if self.concepts: for k, c in self.concepts.items(): if k == key: return c return self.sheerka.get_by_key(key) def new_concept(self, key, **kwargs): # search in obj if self.obj: if self.obj.key == key: return self.sheerka.new_from_template(self.obj, key, **kwargs) for var_name in self.obj.values: if var_name == key: value = self.obj.get_value(var_name) if isinstance(value, Concept): return self.sheerka.new_from_template(value, key, **kwargs) else: return value if self.concepts: for k, c in self.concepts.items(): if k == key: return self.sheerka.new_from_template(c, key, **kwargs) return self.sheerka.new(key, **kwargs) def log_new(self): if self._logger and not self._logger.disabled: self._logger.debug(f"[{self._id:2}]" + self._tab + str(self)) self._show_stats = True def log(self, message, who=None): if self._logger and not self._logger.disabled: self._logger.debug(f"[{self._id:2}]" + self._tab + (f"[{who}] " if who else "") + str(message)) def log_error(self, message, who=None, exc=None): self.errors.append(exc or message) if self._logger and not self._logger.disabled: self._logger.exception(f"[{self._id:2}]" + self._tab + (f"[{who}] " if who else "") + str(message)) def log_result(self, return_values): if not self._logger or not self._logger.isEnabledFor(logging.DEBUG): return if len(return_values) == 0: self._logger.debug(self._tab + "No return value") for r in return_values: to_str = self.return_value_to_str(r) self._logger.debug(f"[{self._id:2}]" + self._tab + "-> " + to_str) def get_debugger(self, who, method_name, new_debug_id=True): return self.sheerka.get_debugger(self, who, method_name, new_debug_id) # TODO: TO REMOVE def debug(self, who, method_name, variable_name, text, is_error=False): activated = self.sheerka.debug_activated_for(who) if activated: str_text = pp.pformat(text) color = 'red' if is_error else 'green' if "\n" not in str(str_text): self.sheerka.debug( f"[{self._id:3}] {CCM[color]}{who}.{method_name}.{variable_name}: {CCM['reset']}{str_text}") else: self.sheerka.debug(f"[{self._id:3}] {CCM[color]}{who}.{method_name}.{variable_name}: {CCM['reset']}") self.sheerka.debug(str_text) # TODO: TO REMOVE def debug_entering(self, who, method_name, **kwargs): if self.sheerka.debug_activated_for(who): str_text = pp.pformat(kwargs) if "\n" not in str(str_text): self.sheerka.debug( f"[{self._id:3}] {CCM['blue']}Entering {who}.{method_name} with {CCM['reset']}{str_text}") else: self.sheerka.debug(f"[{self._id:3}] {CCM['blue']}Entering {who}.{method_name}:{CCM['reset']}") self.sheerka.debug(f"[{self._id:3}] {str_text}") # TODO: TO REMOVE def debug_log(self, who, text): if self.sheerka.debug_activated_for(who): self.sheerka.debug(f"[{self._id:3}] {CCM['blue']}{text}{CCM['reset']}") def get_parent(self): return self._parent def in_context(self, concept_key): return concept_key in self.protected_hints or \ concept_key in self.global_hints or \ concept_key in self.private_hints def in_current_context(self, concept_key): return concept_key in self.protected_hints or concept_key in self.private_hints def in_private_context(self, concept_key): return concept_key in self.private_hints def add_to_private_hints(self, concept_key): self.private_hints.add(concept_key) def add_to_protected_hints(self, concept_key): self.protected_hints.add(concept_key) def add_to_global_hints(self, concept_key): self.global_hints.add(concept_key) @staticmethod def _is_return_value(obj): return isinstance(obj, Concept) and obj.key == str(BuiltinConcepts.RETURN_VALUE) def _at_least_one_success(self, return_values): status = False for ret_val in return_values: if not self._is_return_value(ret_val): return None status |= ret_val.status return status def _all_success(self, return_values): status = True for ret_val in return_values: if not self._is_return_value(ret_val): return None status &= ret_val.status return status def get_status(self): # In the function, I cannot use sheerka.isinstance() as self.sheerka may not be initialized # This is the case when ExecutionContext is deserialized if "return_values" not in self.values: return None if hasattr(self.values["return_values"], "__iter__"): values = self.values["return_values"] if len(values) == 0: return None if isinstance(values, str): return "No Match" if values == NO_MATCH else values if isinstance(values[0], dict): for result in values: if "return_value" not in result: return None if self._is_return_value(result["return_value"]): return result["return_value"].status return "No Match" else: return self._at_least_one_success(self.values["return_values"]) else: ret_val = self.values["return_values"] if not isinstance(ret_val, Concept) or not ret_val.key == str(BuiltinConcepts.RETURN_VALUE): return None if ret_val.status: return True if isinstance(ret_val.body, ParserResultConcept): return "Almost" return False def as_bag(self): """ Creates a dictionary with the useful properties of the concept It quicker to implement than creating the actual property mechanism with @property And it removes the visibility from the other attributes/methods """ bag = {} for prop in ("id", "who", "action", "desc", "obj", "inputs", "values", "concepts"): bag[prop] = getattr(self, prop) bag["context"] = self.action_context for prop in ("desc", "obj", "inputs", "values", "concepts"): bag[prop] = getattr(self, prop) bag["status"] = self.get_status() bag["elapsed"] = self.elapsed bag["elapsed_str"] = self.elapsed_str bag["digest"] = self.event.get_digest() if self.event else None bag["_children"] = self._children return bag @staticmethod def return_value_to_str(r): value = str(r.value) if len(value) > 50: value = value[:47] + "..." to_str = f"ReturnValue(who={r.who}, status={r.status}, value={value})" return to_str def get_format_instructions(self): return self._format_instructions def set_format_instructions(self, instructions): self._format_instructions = instructions def get_parents(self, predicate=None): """ Gets all the parents that match the given predicate :param predicate: :return: """ return list(self.search(predicate, None, False)) def search(self, predicate=None, get_obj=None, start_with_self=False, stop=None): """ Iter thru execution context parent and return the list of obj :param predicate: what execution context to keep :param get_obj: lambda to compute what to return :param start_with_self: include the current execution context in the search :param stop: condition to stop :return: """ current = self if start_with_self else self._parent while current: if predicate is None or predicate(current): yield current if get_obj is None else get_obj(current) if stop and stop(current): break current = current._parent def has_parent(self, context_id): current = self while current._parent: current = current._parent if current.id == context_id: return True if current.id < context_id: return False return False