Refactored ExecutionContext serialization (added sheerkapickle) and added History management

This commit is contained in:
2020-01-31 18:58:03 +01:00
parent fed0735eb9
commit b9afcba61f
31 changed files with 1546 additions and 518 deletions
@@ -0,0 +1,99 @@
from core.builtin_concepts import BuiltinConcepts, ErrorConcept
from core.concept import Concept
from sdp.sheerkaDataProvider import SheerkaDataProviderDuplicateKeyError
CONCEPT_LEXER_PARSER_CLASS = "parsers.ConceptLexerParser.ConceptLexerParser"
class SheerkaCreateNewConcept:
"""
Manage the creation of a new concept
"""
def __init__(self, sheerka):
self.sheerka = sheerka
self.logger_name = self.create_new_concept.__name__
def create_new_concept(self, context, concept: Concept, logger=None):
"""
Adds a new concept to the system
:param context:
:param concept: DefConceptNode
:param logger
:return: digest of the new concept
"""
logger = logger or self.sheerka.log
concept.init_key()
concepts_definitions = None
init_ret_value = None
# checks for duplicate concepts
# TODO checks if it exists in cache first
if self.sheerka.sdp.exists(self.sheerka.CONCEPTS_ENTRY, concept.key, concept.get_digest()):
error = SheerkaDataProviderDuplicateKeyError(self.sheerka.CONCEPTS_ENTRY + "." + concept.key, concept)
return self.sheerka.ret(
self.logger_name,
False,
self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_DEFINED, body=concept),
error.args[0])
# set id before saving in db
self.sheerka.set_id_if_needed(concept, False)
# add the BNF if known
if concept.bnf:
concepts_definitions = self.sheerka.get_concept_definition()
concepts_definitions[concept] = concept.bnf
# check if it's a valid BNF or whether it breaks the known rules
concept_lexer_parser = self.sheerka.parsers[CONCEPT_LEXER_PARSER_CLASS]()
with context.push(self.sheerka.name, desc=f"Initializing concept definition for {concept}") as sub_context:
sub_context.concepts[concept.key] = concept # the concept is not in the real cache yet
sub_context.log_new(logger)
init_ret_value = concept_lexer_parser.initialize(sub_context, concepts_definitions)
sub_context.add_values(return_values=init_ret_value)
if not init_ret_value.status:
return self.sheerka.ret(self.logger_name, False, ErrorConcept(init_ret_value.value))
# save the new concept in sdp
try:
# TODO : needs to make these calls atomic (or at least one single call)
self.sheerka.sdp.add(
context.event.get_digest(),
self.sheerka.CONCEPTS_ENTRY,
concept,
use_ref=True)
self.sheerka.sdp.add(
context.event.get_digest(),
self.sheerka.CONCEPTS_BY_ID_ENTRY,
{concept.id: concept.get_digest()},
is_ref=True)
if concepts_definitions is not None:
self.sheerka.sdp.set(
context.event.get_digest(),
self.sheerka.CONCEPTS_DEFINITIONS_ENTRY,
concepts_definitions,
use_ref=True)
except SheerkaDataProviderDuplicateKeyError as error:
context.log_error(logger, "Failed to create a new concept.", who=self.logger_name)
return self.sheerka.ret(
self.logger_name,
False,
self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_DEFINED, body=concept),
error.args[0])
# Updates the caches
self.sheerka.cache_by_key[concept.key] = self.sheerka.sdp.get_safe(self.sheerka.CONCEPTS_ENTRY, concept.key)
self.sheerka.cache_by_id[concept.id] = concept
if init_ret_value is not None and init_ret_value.status:
self.sheerka.concepts_grammars = init_ret_value.body
# process the return in needed
ret = self.sheerka.ret(self.logger_name, True, self.sheerka.new(BuiltinConcepts.NEW_CONCEPT, body=concept))
return ret
+68
View File
@@ -0,0 +1,68 @@
from core.builtin_concepts import BuiltinConcepts
from core.concept import Concept
class SheerkaDump:
def __init__(self, sheerka):
self.sheerka = sheerka
def dump_concepts(self):
lst = self.sheerka.sdp.list(self.sheerka.CONCEPTS_ENTRY)
for item in lst:
if hasattr(item, "__iter__"):
for i in item:
self.sheerka.log.info(i)
else:
self.sheerka.log.info(item)
def dump_definitions(self):
defs = self.sheerka.sdp.get(self.sheerka.CONCEPTS_DEFINITIONS_ENTRY)
self.sheerka.log.info(defs)
def dump_desc(self, *concept_names):
first = True
for concept_name in concept_names:
if isinstance(concept_name, Concept):
concepts = concept_name
else:
concepts = self.sheerka.get(concept_name)
if self.sheerka.isinstance(concepts, BuiltinConcepts.UNKNOWN_CONCEPT):
self.sheerka.log.error(f"Concept '{concept_name}' is unknown")
return False
if not hasattr(concepts, "__iter__"):
concepts = [concepts]
for c in concepts:
if not first:
self.sheerka.log.info("")
self.sheerka.log.info(f"name : {c.name}")
self.sheerka.log.info(f"bnf : {c.metadata.definition}")
self.sheerka.log.info(f"key : {c.key}")
self.sheerka.log.info(f"body : {c.metadata.body}")
self.sheerka.log.info(f"value : {c.body}")
self.sheerka.log.info(f"digest : {c.get_digest()}")
first = False
def dump_history(self, page=20, start=0):
count = 0
resolved_page = page if page > 0 else 50
page_count = 0
while count < page if page > 0 else True:
history = self.sheerka.history(resolved_page, start + page_count * resolved_page)
try:
h = next(history)
except StopIteration:
break
while True:
try:
if h.event.user != self.sheerka.name:
self.sheerka.log.info(h)
count += 1
h = next(history)
except StopIteration:
break
page_count += 1
@@ -0,0 +1,195 @@
from core.builtin_concepts import BuiltinConcepts
from core.concept import Concept, DoNotResolve, ConceptParts
import core.builtin_helpers
CONCEPT_EVALUATION_STEPS = [
BuiltinConcepts.BEFORE_EVALUATION,
BuiltinConcepts.EVALUATION,
BuiltinConcepts.AFTER_EVALUATION]
class SheerkaEvaluateConcept:
def __init__(self, sheerka):
self.sheerka = sheerka
self.logger_name = self.evaluate_concept.__name__
def initialize_concept_asts(self, context, concept: Concept, logger=None):
"""
Updates the codes of the newly created concept
Basically, it runs the parsers on all parts
:param concept:
:param context:
:param logger:
:return:
"""
steps = [BuiltinConcepts.BEFORE_PARSING, BuiltinConcepts.PARSING, BuiltinConcepts.AFTER_PARSING]
for part_key in ConceptParts:
if part_key in concept.compiled:
continue
source = getattr(concept.metadata, part_key.value)
if source is None or not isinstance(source, str):
continue
if source.strip() == "":
concept.compiled[part_key] = DoNotResolve(source)
else:
with context.push(desc=f"Initializing compiled for {part_key}") as sub_context:
sub_context.log_new(logger)
sub_context.add_inputs(source=source)
to_parse = self.sheerka.ret(context.who, True,
self.sheerka.new(BuiltinConcepts.USER_INPUT, body=source))
res = self.sheerka.execute(sub_context, to_parse, steps, logger)
concept.compiled[part_key] = res
sub_context.add_values(return_values=res)
for prop, default_value in concept.metadata.props:
if prop in concept.compiled:
continue
if default_value is None or not isinstance(default_value, str):
continue
if default_value.strip() == "":
concept.compiled[prop] = DoNotResolve(default_value)
else:
with context.push(desc=f"Initializing AST for property {prop}") as sub_context:
sub_context.log_new(logger)
sub_context.add_inputs(source=default_value)
to_parse = self.sheerka.ret(context.who, True,
self.sheerka.new(BuiltinConcepts.USER_INPUT, body=default_value))
res = self.sheerka.execute(context, to_parse, steps)
concept.compiled[prop] = res
sub_context.add_values(return_values=res)
# Updates the cache of concepts when possible
if concept.key in self.sheerka.cache_by_key:
entry = self.sheerka.cache_by_key[concept.key]
if isinstance(entry, list):
# TODO : manage when there are multiple entries
pass
else:
self.sheerka.cache_by_key[concept.key].compiled = concept.compiled
def resolve(self, context, to_resolve, current_prop, current_concept, logger):
if isinstance(to_resolve, DoNotResolve):
return to_resolve.value
desc = f"Evaluating {current_prop} (concept={current_concept})"
context.log(logger, desc, self.logger_name)
with context.push(desc=desc, obj=current_concept) as sub_context:
sub_context.log_new(logger)
# when it's a concept, evaluate it
if isinstance(to_resolve, Concept) and \
not context.sheerka.isinstance(to_resolve, BuiltinConcepts.RETURN_VALUE):
evaluated = self.evaluate_concept(sub_context, to_resolve, logger)
sub_context.add_values(return_values=evaluated)
if evaluated.key == to_resolve.key:
return evaluated
else:
error = evaluated
# otherwise, execute all return values to find out what is the value
else:
r = self.sheerka.execute(sub_context, to_resolve, CONCEPT_EVALUATION_STEPS, logger)
one_r = core.builtin_helpers.expect_one(context, r)
sub_context.add_values(return_values=one_r)
if one_r.status:
return one_r.value
else:
error = one_r.value
return self.sheerka.new(BuiltinConcepts.CONCEPT_EVAL_ERROR,
body=error,
concept=current_concept,
property_name=current_prop)
def resolve_list(self, context, list_to_resolve, current_prop, current_concept, logger):
"""When dealing with a list, there are two possibilities"""
# It may be a list of ReturnValueConcept to execute (always the case for metadata)
# or a list of single values (may be the case for properties)
# in this latter case, all values are to be processed one by one and a list should be returned
if len(list_to_resolve) == 0:
return []
if self.sheerka.isinstance(list_to_resolve[0], BuiltinConcepts.RETURN_VALUE):
return self.resolve(context, list_to_resolve, current_prop, current_concept, logger)
res = []
for to_resolve in list_to_resolve:
# sanity check
if self.sheerka.isinstance(to_resolve, BuiltinConcepts.RETURN_VALUE):
return self.sheerka.new(BuiltinConcepts.CONCEPT_EVAL_ERROR,
body="Mix between real values and return values",
concept=current_concept,
property_name=current_prop)
r = self.resolve(context, to_resolve, current_prop, current_concept, logger)
if self.sheerka.isinstance(r, BuiltinConcepts.CONCEPT_EVAL_ERROR):
return r
res.append(r)
return res
def evaluate_concept(self, context, concept: Concept, logger=None):
"""
Evaluation a concept
It means that if the where clause is True, will evaluate the body
:param context:
:param concept:
:param logger:
:return: value of the evaluation or error
"""
logger = logger or self.sheerka.log
if concept.metadata.is_evaluated:
return concept
# WHERE condition should already be validated by the parser.
# It's a mandatory condition for the concept before it can be recognized
#
# TODO : Validate the PRE condition
#
self.initialize_concept_asts(context, concept, logger)
# to make sure of the order, it don't use ConceptParts.get_parts()
# props must be evaluated first
all_metadata_to_eval = ["props", "where", "pre", "post", "body"]
for metadata_to_eval in all_metadata_to_eval:
if metadata_to_eval == "props":
for prop_name in (p for p in concept.props if p in concept.compiled):
prop_ast = concept.compiled[prop_name]
if isinstance(prop_ast, list):
# Do not send the current concept for the properties
resolved = self.resolve_list(context, prop_ast, prop_name, None, logger)
else:
# Do not send the current concept for the properties
resolved = self.resolve(context, prop_ast, prop_name, None, logger)
if context.sheerka.isinstance(resolved, BuiltinConcepts.CONCEPT_EVAL_ERROR):
resolved.set_prop("concept", concept) # since current concept was not sent
return resolved
else:
concept.set_prop(prop_name, resolved)
else:
part_key = ConceptParts(metadata_to_eval)
if part_key in concept.compiled and concept.compiled[part_key] is not None:
metadata_ast = concept.compiled[part_key]
resolved = self.resolve(context, metadata_ast, part_key, concept, logger)
if context.sheerka.isinstance(resolved, BuiltinConcepts.CONCEPT_EVAL_ERROR):
return resolved
else:
concept.values[part_key] = resolved
#
# TODO : Validate the POST condition
#
concept.init_key() # only does it if needed
concept.metadata.is_evaluated = True
return concept
+254
View File
@@ -0,0 +1,254 @@
from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept
import core.utils
class SheerkaExecute:
"""
Manage the execution of a process flow
"""
def __init__(self, sheerka):
self.sheerka = sheerka
def call_parsers(self, execution_context, return_values, logger=None):
# return_values must be a list
if not isinstance(return_values, list):
return_values = [return_values]
# first make the distinguish between what is for the parsers and what is not
result = []
to_process = []
for r in return_values:
if not r.status or not self.sheerka.isinstance(r.body, BuiltinConcepts.USER_INPUT):
result.append(r)
else:
to_process.append(r)
if not to_process:
return result
# keep track of the originals user inputs, as they need to be removed at the end
user_inputs = to_process[:]
# group the parsers by priorities
instantiated_parsers = [parser(sheerka=self.sheerka) for parser in self.sheerka.parsers.values()]
grouped_parsers = {}
for parser in [p for p in instantiated_parsers if p.enabled]:
if logger:
parser.log = logger
grouped_parsers.setdefault(parser.priority, []).append(parser)
sorted_priorities = sorted(grouped_parsers.keys(), reverse=True)
stop_processing = False
for priority in sorted_priorities:
inputs_for_this_group = to_process[:]
for parser in grouped_parsers[priority]:
return_value_success_found = False
for return_value in inputs_for_this_group:
to_parse = return_value.body.body \
if self.sheerka.isinstance(return_value.body, BuiltinConcepts.USER_INPUT) \
else return_value.body
# if self.sheerka.log.isEnabledFor(logging.DEBUG):
# debug_text = "'" + to_parse + "'" if isinstance(to_parse, str) \
# else "'" + BaseParser.get_text_from_tokens(to_parse) + "' as tokens"
# execution_context.log(logger or self.sheerka.log, f"Parsing {debug_text}")
with execution_context.push(desc=f"Parsing using {parser.name}") as sub_context:
sub_context.add_inputs(to_parse=to_parse)
res = parser.parse(sub_context, to_parse)
if res is not None:
if hasattr(res, "__iter__"):
for r in res:
if r is None:
continue
r.parents = [return_value]
result.append(r)
if self.sheerka.isinstance(r.body, BuiltinConcepts.PARSER_RESULT):
to_process.append(r)
if r.status:
return_value_success_found = True
else:
res.parents = [return_value]
result.append(res)
if self.sheerka.isinstance(res.body, BuiltinConcepts.PARSER_RESULT):
to_process.append(res)
if res.status:
return_value_success_found = True
sub_context.add_values(return_values=res)
if return_value_success_found:
stop_processing = True
break # Stop the other return_values (but not the other parsers with the same priority)
if stop_processing:
break # Do not try the other priorities if a match is found
result = core.utils.remove_list_from_list(result, user_inputs)
return result
def call_evaluators(self, execution_context, return_values, process_step, evaluation_context=None, logger=None):
# return_values must be a list
if not isinstance(return_values, list):
return_values = [return_values]
# Evaluation context are contexts that may modify the behaviour of the execution
# For example, a concept to indicate that the value is not wanted
# Or a concept to indicate that we want the letter form of the response
# But first, they need to be transformed into return values
if evaluation_context is None:
evaluation_return_values = []
else:
evaluation_return_values = [self.sheerka.ret(execution_context.who, True, c) for c in evaluation_context]
# add the current step as part as the evaluation context
evaluation_return_values.append(self.sheerka.ret(execution_context.who, True, self.sheerka.new(process_step)))
# the pool of return values are the mix
return_values.extend(evaluation_return_values)
# group the evaluators by priority and sort them
# The first one to be applied will be the one with the highest priority
grouped_evaluators = {}
instantiated_evaluators = [e_class() for e_class in self.sheerka.evaluators]
# pre-process evaluators if needed
instantiated_evaluators = self._preprocess_evaluators(execution_context, instantiated_evaluators)
for evaluator in [e for e in instantiated_evaluators if e.enabled and process_step in e.steps]:
if logger:
evaluator.log = logger
grouped_evaluators.setdefault(evaluator.priority, []).append(evaluator)
# order the groups by priority, the higher first
sorted_priorities = sorted(grouped_evaluators.keys(), reverse=True)
# process
iteration = 0
while True:
with execution_context.push(desc=f"iteration #{iteration}", iteration=iteration) as iteration_context:
simple_digest = return_values[:]
iteration_context.add_inputs(return_values=simple_digest)
for priority in sorted_priorities:
original_items = return_values[:]
evaluated_items = []
to_delete = []
for evaluator in grouped_evaluators[priority]:
evaluator = self._preprocess_evaluators(execution_context, evaluator.__class__()) # fresh copy
sub_context_desc = f"Evaluating using {evaluator.name} ({priority=})"
with iteration_context.push(desc=sub_context_desc) as sub_context:
sub_context.add_inputs(return_values=original_items)
# process evaluators that work on one simple return value at the time
from evaluators.BaseEvaluator import OneReturnValueEvaluator
if isinstance(evaluator, OneReturnValueEvaluator):
debug_result = []
for item in original_items:
if evaluator.matches(sub_context, item):
result = evaluator.eval(sub_context, item)
if result is None:
debug_result.append({"input": item, "return_value": None})
continue
to_delete.append(item)
if isinstance(result, list):
evaluated_items.extend(result)
elif isinstance(result, ReturnValueConcept):
evaluated_items.append(result)
else:
error = self.sheerka.new(BuiltinConcepts.INVALID_RETURN_VALUE, body=result,
evaluator=evaluator)
result = self.sheerka.ret("sheerka.process", False, error, parents=[item])
evaluated_items.append(result)
debug_result.append({"input": item, "return_value": result})
else:
debug_result.append({"input": item, "return_value": "** No Match **"})
sub_context.add_values(return_values=debug_result)
# process evaluators that work on all return values
else:
if evaluator.matches(sub_context, original_items):
results = evaluator.eval(sub_context, original_items)
if results is None:
continue
if not isinstance(results, list):
results = [results]
for result in results:
evaluated_items.append(result)
to_delete.extend(result.parents)
sub_context.add_values(return_values=results)
else:
sub_context.add_values(return_values="** No Match **")
return_values = evaluated_items
return_values.extend([item for item in original_items if item not in to_delete])
iteration_context.add_values(return_values=return_values[:])
# have we done something ?
to_compare = return_values[:]
if simple_digest == to_compare:
break
# inc the iteration and continue
iteration += 1
# remove all evaluation context that are not reduced
return_values = core.utils.remove_list_from_list(return_values, evaluation_return_values)
return return_values
def execute(self, execution_context, return_values, execution_steps, logger=None):
"""
Executes process for all initial contexts
:param execution_context:
:param return_values:
:param execution_steps:
:param logger: logger to use (if not directly called by sheerka)
:return:
"""
for step in execution_steps:
copy = return_values[:] if hasattr(return_values, "__iter__") else [return_values]
with execution_context.push(step=step, iteration=0, desc=f"{step=}") as sub_context:
sub_context.log(logger or self.sheerka.log, f"{step=}, context='{sub_context}'")
if step == BuiltinConcepts.PARSING:
return_values = self.call_parsers(sub_context, return_values, logger)
else:
return_values = self.call_evaluators(sub_context, return_values, step, None, logger)
if copy != return_values:
sub_context.log_result(logger or self.sheerka.log, return_values)
sub_context.add_values(return_values=return_values)
return return_values
def _preprocess_evaluators(self, context, evaluators):
if not context.preprocess:
return evaluators
if not hasattr(evaluators, "__iter__"):
single_one = True
evaluators = [evaluators]
else:
single_one = False
for preprocess in context.preprocess:
for e in evaluators:
if preprocess.props["name"].value == e.name:
for prop, value in preprocess.props.items():
if prop == "name":
continue
if hasattr(e, prop):
setattr(e, prop, value.value)
return evaluators[0] if single_one else evaluators
@@ -0,0 +1,74 @@
from collections import namedtuple
from sdp.sheerkaDataProvider import Event
hist = namedtuple("History", "text status") # tests purposes only
class History:
def __init__(self, event: Event, result):
self.event = event
self.result = result
self._status = None
def __str__(self):
msg = f"{self.event.get_digest()} {self.event.date.strftime('%d/%m/%Y %H:%M:%S')} : {self.event.message}"
status = self.status
if status is not None:
msg += f" => {status}"
return msg
def __repr__(self):
return f"event={self.event!r}, status={self.status}, result={self.result}"
def __eq__(self, other):
if id(self) == id(other):
return True
if isinstance(other, hist):
return self.event.message == other.text and self.status == other.status
if not isinstance(other, History):
return False
return self.event == other.event and self.result == other.result
@property
def status(self):
if self._status:
return self._status
if not self.result or "return_values" not in self.result.values:
return
if hasattr(self.result.values["return_values"], "__iter__"):
if len(self.result.values["return_values"]) != 1:
self._status = False
return self._status
else:
self._status = self.result.values["return_values"][0].status
return self._status
else:
self._status = self.result.values["return_values"].status
return self._status
class SheerkaHistoryManager:
def __init__(self, sheerka):
self.sheerka = sheerka
def history(self, depth_or_digest, start):
"""
Load history
:param depth_or_digest: number of items or digest
:param start:
:return:
"""
events = list(self.sheerka.sdp.load_events(depth_or_digest, start))
for event in events:
try:
result = self.sheerka.sdp.load_result(self.sheerka, event.get_digest())
except (IOError, KeyError):
result = None
yield History(event, result)
@@ -0,0 +1,83 @@
from core.builtin_concepts import BuiltinConcepts, ErrorConcept
from core.concept import Concept
GROUP_PREFIX = 'All_'
class SheerkaSetsManager:
def __init__(self, sheerka):
self.sheerka = sheerka
self.logger_name = self.add_concept_to_set.__name__
def add_concept_to_set(self, context, concept, concept_set, logger=None):
"""
Add an entry in sdp to tell that concept isa concept_set
:param context:
:param concept:
:param concept_set:
:param logger:
:return:
"""
logger = logger or self.sheerka.log
context.log(logger, f"Adding concept {concept} to set {concept_set}", who=self.logger_name)
assert concept.id
assert concept_set.id
try:
ret = self.sheerka.sdp.add_unique(context.event.get_digest(), GROUP_PREFIX + concept_set.id, concept.id)
if ret == (None, None): # concept already in set
return self.sheerka.ret(
self.logger_name,
False,
self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_IN_SET, body=concept, concept_set=concept_set))
else:
return self.sheerka.ret(self.logger_name, True, self.sheerka.new(BuiltinConcepts.SUCCESS))
except Exception as error:
context.log_error(logger, "Failed to add to set.", who=self.logger_name)
return self.sheerka.ret(self.logger_name, False, ErrorConcept(error), error.args[0])
def get_set_elements(self, concept):
"""
Concept is supposed to be a set
Returns all elements if the set
:param concept:
:return:
"""
assert concept.id
ids = self.sheerka.sdp.get_safe(GROUP_PREFIX + concept.id)
if ids is None:
return self.sheerka.new(BuiltinConcepts.NOT_A_SET, body=concept)
elements = [self.sheerka.get_by_id(element_id) for element_id in ids]
return elements
def isa(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:
"""
if isinstance(a, BuiltinConcepts): # common KSI error ;-)
raise SyntaxError("Remember that the first parameter of isinstance MUST be a concept")
assert isinstance(a, Concept)
assert isinstance(b, Concept)
# TODO, first check the 'isa' property of a
return self.sheerka.sdp.exists(GROUP_PREFIX + b.id, a.id)
def isagroup(self, concept):
"""True if exists All_<concept_id> in sdp"""
if not concept.id:
return None
res = self.sheerka.sdp.get_safe(GROUP_PREFIX + concept.id)
return res is not None