503 lines
16 KiB
Python
503 lines
16 KiB
Python
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
|
|
|
|
|