Files
Sheerka-Old/src/core/sheerka/services/SheerkaEvaluateConcept.py
T

588 lines
25 KiB
Python

from dataclasses import dataclass
from core.builtin_concepts import BuiltinConcepts
from core.builtin_helpers import expect_one, only_successful, parse_unrecognized, evaluate
from core.concept import Concept, DoNotResolve, ConceptParts, InfiniteRecursionResolved, NotInit, ensure_concept
from core.sheerka.services.SheerkaExecute import ParserInput
from core.sheerka.services.sheerka_service import BaseService
from core.tokenizer import Tokenizer
from core.utils import unstr_concept
from parsers.ExpressionParser import ExpressionParser, TrueifyVisitor
CONCEPT_EVALUATION_STEPS = [
BuiltinConcepts.BEFORE_EVALUATION,
BuiltinConcepts.EVALUATION,
BuiltinConcepts.AFTER_EVALUATION]
@dataclass
class WhereClauseDef:
concept: Concept # concept on which the where clause is applied
clause: str # original where clause
trueified: str # modified where clause (where unresolvable variables are removed)
prop: str # variable to test
compiled: object # trueified where clause Python compiled
class SheerkaEvaluateConcept(BaseService):
NAME = "EvaluateConcept"
def __init__(self, sheerka):
super().__init__(sheerka)
def initialize(self):
self.sheerka.bind_service_method(self.evaluate_concept, True)
self.sheerka.bind_service_method(self.set_auto_eval, True)
@staticmethod
def infinite_recursion_detected(context, concept):
"""
Browse the parents, looking for another evaluation of the same concept
:param context:
:param concept:
:return:
"""
if concept is None:
return False
parent = context.get_parent()
while parent is not None:
if parent.who == context.who and parent.action == BuiltinConcepts.EVALUATING_CONCEPT and \
parent.obj == concept and parent.obj.compiled == concept.compiled:
return True
parent = parent.get_parent()
return False
@staticmethod
def get_infinite_recursion_resolution(obj):
if isinstance(obj, InfiniteRecursionResolved):
return obj
if isinstance(obj, Concept) and isinstance(obj.body, InfiniteRecursionResolved):
return obj.body
return None
@staticmethod
def apply_ret(concept):
"""
Check if a concept has its RET part defined
If True, returns it
:param concept:
:return:
"""
return concept.get_value(ConceptParts.RET) if ConceptParts.RET in concept.values else concept
@staticmethod
def get_needed_metadata(concept, concept_part, check_vars, check_body):
"""
Check if the concept_part has to be evaluated
It also checks if the variables and the body need to be evaluated prior to it
:param concept:
:param concept_part:
:param check_vars:
:param check_body:
:return:
"""
ret = []
vars_needed = False
body_needed = False
if concept_part in concept.compiled and concept.compiled[concept_part] is not None:
concept_part_source = getattr(concept.metadata, concept_part.value)
assert concept_part_source is not None
tokens = [t.str_value for t in Tokenizer(concept_part_source)]
if check_vars:
for var_name in (v[0] for v in concept.metadata.variables):
if var_name in tokens:
vars_needed = True
ret.append("variables")
break
if check_body and "self" in tokens:
body_needed = True
ret.append("body")
ret.append(concept_part.value)
return ret, vars_needed, body_needed
@staticmethod
def get_where_clause_def(context, concept, var_name):
"""
Returns the compiled code to be executed
:param context:
:param concept:
:param var_name:
:return:
"""
if concept.metadata.where is None or concept.metadata.where.strip() == "":
return None
ret = ExpressionParser().parse(context, ParserInput(concept.metadata.where))
if not ret.status:
# TODO: manage invalid where clause
return None
expr = ret.body.body
to_trueify = [v[0] for v in concept.metadata.variables if v[0] != var_name]
trueified_where = str(TrueifyVisitor(to_trueify, [var_name]).visit(expr))
tokens = [t.str_value for t in Tokenizer(trueified_where)]
if var_name in tokens:
compiled = None
try:
compiled = compile(trueified_where, "<where clause>", "eval")
except Exception:
pass
return WhereClauseDef(concept, concept.metadata.where, trueified_where, var_name, compiled)
else:
return None
def apply_where_clause(self, context, where_clause_def, return_values):
"""
Apply intermediate where clause when evaluating concept variables
:param context:
:param where_clause_def:
:param return_values:
:return:
"""
ret = []
for r in [r for r in return_values if r.status]:
if where_clause_def.compiled:
try:
if eval(where_clause_def.compiled, {where_clause_def.prop: self.sheerka.objvalue(r)}):
ret.append(r)
except NameError:
ret.append(r) # it cannot be solved unitary, let's give a chance to the global where condition
else:
# it means that the where condition is an expression that needs to be executed
evaluation_res = evaluate(context,
where_clause_def.trueified,
desc=f"Apply where clause on '{where_clause_def.prop}'",
expect_success=True,
is_question=True,
stm={where_clause_def.prop: r.body})
one_res = expect_one(context, evaluation_res)
if one_res.status:
value = context.sheerka.objvalue(one_res)
if isinstance(value, bool) and value:
ret.append(r)
if len(ret) > 0:
return ret
return self.sheerka.new(BuiltinConcepts.CONDITION_FAILED,
body=where_clause_def.clause,
concept=where_clause_def.concept,
prop=where_clause_def.prop)
def manage_infinite_recursion(self, context):
"""
We look for the fist parent that has a body that means something
We use the eval function with no local or global
:param context:
:return:
"""
parent = context
concepts_found = set()
while parent and parent.obj:
if parent.who == context.who and parent.action == BuiltinConcepts.EVALUATING_CONCEPT:
body = parent.obj.metadata.body
try:
return self.sheerka.ret(self.NAME, True, InfiniteRecursionResolved(eval(body)))
except Exception:
pass
concepts_found.add(parent.obj)
parent = parent.get_parent()
return self.sheerka.ret(
self.NAME,
False,
self.sheerka.new(BuiltinConcepts.CHICKEN_AND_EGG, body=concepts_found))
def initialize_concept_asts(self, context, concept: Concept):
"""
Updates the codes of the newly created concept
Basically, it runs the parsers on all parts
:param concept:
:param context:
:return:
"""
def is_only_successful(r):
# return False
return context.sheerka.isinstance(r, BuiltinConcepts.RETURN_VALUE) and \
context.sheerka.isinstance(r.body, BuiltinConcepts.ONLY_SUCCESSFUL)
def parse_token_concept(s):
if s.startswith("c:") and (identifier := unstr_concept(s)) != (None, None):
return self.sheerka.resolve(identifier)
return None
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 not isinstance(source, str):
raise Exception("Invalid concept init. metadata must be a string")
if source.strip() == "":
concept.compiled[part_key] = DoNotResolve(source)
else:
# first case, when the metadata references another concept via c:xxx: keyword
if concept_found := parse_token_concept(source):
context.log(f"Recognized concept '{concept_found}'", self.NAME)
concept.compiled[part_key] = concept_found
else:
res = parse_unrecognized(context,
source,
parsers="all",
prop=part_key,
filter_func=only_successful)
concept.compiled[part_key] = res.body.body if is_only_successful(res) else res
for var_name, default_value in concept.metadata.variables:
if var_name in concept.compiled:
continue
if default_value is None:
continue
if not isinstance(default_value, str):
raise Exception("Invalid concept init. variable metadata must be a string")
if default_value.strip() == "":
concept.compiled[var_name] = DoNotResolve(default_value)
else:
# first case, when the metadata references another concept via c:xxx: keyword
if concept_found := parse_token_concept(default_value):
context.log(f"Recognized concept '{concept_found}'", self.NAME)
concept.compiled[var_name] = concept_found
else:
res = parse_unrecognized(context,
default_value,
parsers="all",
prop=var_name,
filter_func=only_successful)
concept.compiled[var_name] = res.body.body if is_only_successful(res) else res
# Updates the cache of concepts when possible
if self.sheerka.has_id(concept.id):
self.sheerka.get_by_id(concept.id).compiled = concept.compiled
def resolve(self,
context,
to_resolve,
current_prop,
current_concept,
force_evaluation,
where_clause_def):
"""
Resolve a variable or a Concept
:param context: current execution context
:param to_resolve: Concept or list of ReturnValueConcept to resolve
:param current_prop: current property or ConceptPart
:param current_concept: current concept
:param force_evaluation: Force body evaluation
:param where_clause_def: intermediate where clause for variables
:return:
"""
def get_path(context_, prop_name):
concept_name = f'"{context_.action_context.name}"' if isinstance(context_.action_context, Concept) \
else "'N/A'"
prefix = context_.path if hasattr(context_, "path") else concept_name
value = prop_name.name if isinstance(current_prop, ConceptParts) else prop_name
return prefix + "." + value
if isinstance(to_resolve, DoNotResolve):
return to_resolve.value
# manage infinite loop
if self.infinite_recursion_detected(context, current_concept):
with context.push(BuiltinConcepts.MANAGE_INFINITE_RECURSION,
current_concept,
desc="Infinite recursion detected",
obj=current_concept) as sub_context:
# I create a sub context in order to log what happened
ret_val = self.manage_infinite_recursion(context)
sub_context.add_values(return_values=ret_val)
return ret_val.body
path = get_path(context, current_prop)
desc = f"Evaluating {path} (concept={current_concept})"
with context.push(BuiltinConcepts.EVALUATING_ATTRIBUTE,
current_prop,
desc=desc,
obj=current_concept,
path=path) as sub_context:
if force_evaluation:
sub_context.protected_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
if current_prop in (ConceptParts.WHERE, ConceptParts.PRE):
sub_context.protected_hints.add(BuiltinConcepts.EVAL_UNTIL_SUCCESS_REQUESTED)
if current_prop == ConceptParts.WHERE:
sub_context.protected_hints.add(BuiltinConcepts.EVAL_QUESTION_REQUESTED)
# 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)
sub_context.add_values(return_values=evaluated)
if not context.sheerka.is_success(evaluated) and evaluated.key != to_resolve.key:
error = evaluated
else:
return evaluated
# otherwise, execute all return values to find out what is the value
else:
# update short term memory with current concept variables
if current_concept:
for var in current_concept.metadata.variables:
value = current_concept.get_value(var[0])
if value != NotInit:
sub_context.add_to_short_term_memory(var[0], current_concept.get_value(var[0]))
use_copy = [r for r in to_resolve] if hasattr(to_resolve, "__iter__") else to_resolve
r = self.sheerka.execute(sub_context, use_copy, CONCEPT_EVALUATION_STEPS)
if where_clause_def:
# apply intermediate where clause
r = self.apply_where_clause(context, where_clause_def, r)
if self.sheerka.isinstance(r, BuiltinConcepts.CONDITION_FAILED):
return r
else:
one_r = 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 error if self.sheerka.isinstance(error, BuiltinConcepts.CHICKEN_AND_EGG) \
else 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,
force_evaluation,
where_clause_def):
"""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,
force_evaluation,
where_clause_def)
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,
force_evaluation,
where_clause_def)
if self.sheerka.isinstance(r, BuiltinConcepts.CONCEPT_EVAL_ERROR):
return r
res.append(r)
return res
def evaluate_concept(self, context, concept: Concept, eval_body=False, metadata=None):
"""
Evaluation a concept
ie : resolve its body
:param context:
:param concept:
:param eval_body:
:param metadata: list of metadata to evaluate ('pre', 'post'...)
:return: value of the evaluation or error
"""
if concept.metadata.is_evaluated:
return concept
# I cannot use cache because of concept like 'number'.
# They don't have variables, but their values change every time they are instantiated
# TODO: Need to find a way to cache despite of them
# need_body = eval_body or context.in_context(BuiltinConcepts.EVAL_BODY_REQUESTED)
# if need_body and len(concept.metadata.variables) == 0 and context.sheerka.has_id(concept.id):
# from_cache = context.sheerka.get_by_id(concept.id)
# if from_cache.metadata.is_evaluated:
# concept.set_value(ConceptParts.BODY, from_cache.body)
# concept.metadata.is_evaluated = True
# return concept
desc = f"Evaluating concept {concept}"
with context.push(BuiltinConcepts.EVALUATING_CONCEPT, concept, desc=desc, eval_body=eval_body) as sub_context:
if eval_body:
# ask for body evaluation
sub_context.protected_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
# auto evaluate commands
if context.sheerka.isa(concept, context.sheerka.new(BuiltinConcepts.AUTO_EVAL)):
sub_context.protected_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
self.initialize_concept_asts(sub_context, concept)
# to make sure of the order, it don't use ConceptParts.get_parts()
# variables must be evaluated first, body must be evaluated before where
all_metadata_to_eval = metadata or self.compute_metadata_to_eval(sub_context, concept)
for metadata_to_eval in all_metadata_to_eval:
if metadata_to_eval == "variables":
for var_name in (v for v in concept.variables() if v in concept.compiled):
prop_ast = concept.compiled[var_name]
w_clause = self.get_where_clause_def(context, concept, var_name)
# TODO, manage when the where clause cannot be parsed
if isinstance(prop_ast, list):
# Do not send the current concept for the properties
resolved = self.resolve_list(sub_context, prop_ast, var_name, None, True, w_clause)
else:
# Do not send the current concept for the properties
resolved = self.resolve(sub_context, prop_ast, var_name, None, True, w_clause)
if isinstance(resolved, Concept) and not sub_context.sheerka.is_success(resolved):
resolved.set_value("concept", concept) # since current concept was not sent
return resolved
else:
concept.set_value(var_name, resolved)
else:
part_key = ConceptParts(metadata_to_eval)
# do not evaluate where when the body is a set
# Indeed, the way that the where clause is expressed is not a valid python or concept code
if part_key == ConceptParts.WHERE and self.sheerka.isaset(sub_context, concept.body):
continue
if part_key not in concept.compiled or concept.compiled[part_key] is None:
continue
metadata_ast = concept.compiled[part_key]
# if part_key is PRE, POST or WHERE, the concept need to be evaluated
# if we want the predicates to be resolved => so force_eval = True
# otherwise no need to force
force_concept_eval = False if part_key == ConceptParts.BODY else True
# resolve
resolved = self.resolve(sub_context, metadata_ast, part_key, concept, force_concept_eval, None)
# 'FATAL' error is detected, let's stop
if isinstance(resolved, Concept) and not sub_context.sheerka.is_success(resolved):
return resolved
concept.set_value(part_key, self.get_infinite_recursion_resolution(resolved) or resolved)
# validate PRE and WHERE condition
if part_key in (ConceptParts.PRE, ConceptParts.WHERE) and not self.sheerka.objvalue(resolved):
return self.sheerka.new(BuiltinConcepts.CONDITION_FAILED,
body=getattr(concept.metadata, metadata_to_eval),
concept=concept,
prop=part_key)
#
# TODO : Validate the POST condition
#
concept.init_key() # Necessary for old unit tests. To remove someday
if "body" in all_metadata_to_eval:
concept.metadata.is_evaluated = True
# # update the cache for concepts with no variables
# Cannot use cache. See the comment at the beginning of this method
# if len(concept.metadata.variables) == 0:
# self.sheerka.cache_manager.put(self.sheerka.CONCEPTS_BY_ID_ENTRY, concept.id, concept)
if not concept.metadata.is_builtin:
self.sheerka.register_object(sub_context, concept.name, concept)
# manage RET metadata
if sub_context.in_context(BuiltinConcepts.EVAL_BODY_REQUESTED) and ConceptParts.RET in concept.values:
return concept.get_value(ConceptParts.RET)
else:
return concept
def compute_metadata_to_eval(self, context, concept):
to_eval = []
needed, variables, body = self.get_needed_metadata(concept, ConceptParts.PRE, True, True)
to_eval.extend(needed)
if context.in_context(BuiltinConcepts.EVAL_WHERE_REQUESTED) or concept.metadata.need_validation:
# What are the cases where we do not need a validation ?
# see test_sheerka_non_reg::test_i_can_evaluate_bnf_concept_with_where_clause()
# res = sheerka.evaluate_user_input("foobar")
needed, v, b = self.get_needed_metadata(concept, ConceptParts.WHERE, not variables, not body)
variables |= v
body |= b
to_eval.extend(needed)
if context.in_context(BuiltinConcepts.EVAL_BODY_REQUESTED):
needed, v, b = self.get_needed_metadata(concept, ConceptParts.RET, not variables, not body)
variables |= v
body |= b
to_eval.extend(needed)
needed, v, b = self.get_needed_metadata(concept, ConceptParts.POST, not variables, not body)
variables |= v
body |= b
to_eval.extend(needed)
if context.in_context(BuiltinConcepts.EVAL_BODY_REQUESTED):
if not variables:
to_eval.append('variables')
if not body:
to_eval.append("body")
return to_eval
def set_auto_eval(self, context, concept):
"""
add AUTO_EVAL to ISA
:param context:
:param concept:
:return:
"""
ensure_concept(concept)
return self.sheerka.set_isa(context, concept, self.sheerka.new(BuiltinConcepts.AUTO_EVAL))