Files
Sheerka-Old/src/core/sheerka/ExecutionContext.py
T
kodjo 646c428edb Fixed #30 : Add variable support in BNF concept definition
Fixed #31 : Add regex support in BNF Concept
Fixed #33 : Do not memorize object during restore
2021-02-24 17:23:03 +01:00

500 lines
17 KiB
Python

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