import logging import os import pprint import time from core.builtin_concepts import BuiltinConcepts, ParserResultConcept from core.concept import Concept, get_concept_attrs from core.global_symbols import CONTEXT_DISPOSED from core.sheerka.services.SheerkaExecute import NO_MATCH from core.sheerka.services.SheerkaMemory import SheerkaMemory from core.utils import CONSOLE_COLORS_MAP as CCM from sdp.sheerkaDataProvider import Event try: rows, columns = os.popen('stty size', 'r').read().split() except ValueError: rows, columns = 50, 80 pp = pprint.PrettyPrinter(indent=2, width=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 self_debug, self.debug_mode = sheerka.get_context_debug_mode(self.id) self.debug_enabled = self_debug is not None @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, 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) if new.debug_mode is None and self.debug_mode == "protected": new.debug_mode = "protected" new.debug_enabled = True 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, CONTEXT_DISPOSED) self._push._stop = time.time_ns() self._push = None def add_preprocess(self, name, **kwargs): 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): return self.sheerka.get_debugger(self, who, method_name) 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) 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}") 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