Added SheerkaComparisonManager

This commit is contained in:
2020-05-17 20:19:26 +02:00
parent 56e0a9d338
commit 08e3086820
29 changed files with 586 additions and 148 deletions
@@ -0,0 +1,206 @@
from dataclasses import dataclass
from cache.Cache import Cache
from cache.ListCache import ListCache
from core.builtin_concepts import BuiltinConcepts
from core.sheerka.services.sheerka_service import ServiceObj, BaseService
@dataclass
class ComparisonObj(ServiceObj):
"""
Order to store
"""
property: str # property to compare
a: int # id of concept a
b: int # id of concept b
op: str # comparison operation
context: str = "#" # context when the comparison is right
class SheerkaComparisonManager(BaseService):
"""
Manage partitioning of concepts
"""
NAME = "ComparisonManager"
COMPARISON_ENTRY = "Comparison"
RESOLVED_COMPARISON_ENTRY = "Resolved_Comparison"
def __init__(self, sheerka):
super().__init__(sheerka)
@staticmethod
def _compute_key(prop_name, comparison_context):
return f"{prop_name}|{comparison_context}"
@staticmethod
def _compute_weights(comparison_objs):
"""
For every element in greater_than_s, give it a weight
if weight(a) > weight(b) it means that a > b
:param comparison_objs: list of greater than objects
:return:
"""
values = {}
for comparison_obj in comparison_objs:
values[comparison_obj.a] = 1
values[comparison_obj.b] = 1
for _ in range(len(comparison_objs)):
for comparison_obj in comparison_objs:
if comparison_obj.op == ">":
values[comparison_obj.a] = values[comparison_obj.b] + 1
else:
values[comparison_obj.b] = values[comparison_obj.a] + 1
return values
@staticmethod
def _get_partition(weighted_concepts):
res = {}
for k, v in weighted_concepts.items():
res.setdefault(v, []).append(k)
return res
def _inner_add_comparison(self, comparison_obj):
key = self._compute_key(comparison_obj.property, comparison_obj.context)
previous = self.sheerka.cache_manager.get(self.COMPARISON_ENTRY, key)
new = previous.copy() if previous else []
new.append(comparison_obj)
cycles = self.detect_cycles(new)
if cycles:
concepts_in_cycle = [self.sheerka.get_by_id(c) for c in cycles]
chicken_an_egg = self.sheerka.new(BuiltinConcepts.CHICKEN_AND_EGG, body=concepts_in_cycle)
return self.sheerka.ret(self.NAME, False, chicken_an_egg)
self.sheerka.cache_manager.put(self.RESOLVED_COMPARISON_ENTRY, key, self._compute_weights(new))
self.sheerka.cache_manager.put(self.COMPARISON_ENTRY, key, comparison_obj)
return self.sheerka.ret(self.NAME, True, self.sheerka.new(BuiltinConcepts.SUCCESS))
def initialize(self):
cache = ListCache(default=lambda k: self.sheerka.sdp.get(self.COMPARISON_ENTRY, k))
self.sheerka.cache_manager.register_cache(self.COMPARISON_ENTRY, cache, True, True)
cache = Cache()
self.sheerka.cache_manager.register_cache(self.RESOLVED_COMPARISON_ENTRY, cache, persist=False)
self.sheerka.bind_service_method(self, SheerkaComparisonManager.is_greater_than)
self.sheerka.bind_service_method(self, SheerkaComparisonManager.is_less_than)
self.sheerka.bind_service_method(self, SheerkaComparisonManager.get_partition)
self.sheerka.bind_service_method(self, SheerkaComparisonManager.get_concepts_weights)
def is_greater_than(self, context, prop_name, concept_a, concept_b, comparison_context="#"):
"""
Records that the property of concept a is greater than concept b's one
:param context:
:param prop_name:
:param concept_a:
:param concept_b:
:param comparison_context:
:return:
"""
context.log(f"Setting concept {concept_a} is greater than {concept_b}", who=self.NAME)
event_digest = context.event.get_digest()
comparison_obj = ComparisonObj(event_digest, prop_name, concept_a.id, concept_b.id, ">", comparison_context)
return self._inner_add_comparison(comparison_obj)
def is_less_than(self, context, prop_name, concept_a, concept_b, comparison_context="#"):
"""
Records that the property of concept a is lesser than concept b's one
:param context:
:param prop_name:
:param concept_a:
:param concept_b:
:param comparison_context:
:return:
"""
context.log(f"Setting concept {concept_a} is less than {concept_b}", who=self.NAME)
event_digest = context.event.get_digest()
comparison_obj = ComparisonObj(event_digest, prop_name, concept_a.id, concept_b.id, "<", comparison_context)
return self._inner_add_comparison(comparison_obj)
def get_partition(self, prop_name, comparison_context="#"):
weighted_concept = self.get_concepts_weights(prop_name, comparison_context)
return self._get_partition(weighted_concept)
def get_concepts_weights(self, prop_name, comparison_context="#"):
weighted_concept = self.sheerka.cache_manager.get(
self.RESOLVED_COMPARISON_ENTRY,
self._compute_key(prop_name, comparison_context))
if weighted_concept is None:
key = self._compute_key(prop_name, comparison_context)
entries = self.sheerka.cache_manager.get(self.COMPARISON_ENTRY, key)
if entries is None:
return {}
else:
weighted_concept = self._compute_weights(entries)
self.sheerka.cache_manager.put(self.RESOLVED_COMPARISON_ENTRY, key, weighted_concept)
return weighted_concept
@staticmethod
def detect_cycles(comparison_objs):
"""
# Thanks to Divyanshu Mehta for contributing this code
# https://www.geeksforgeeks.org/detect-cycle-in-a-graph/?ref=lbp
:param comparison_objs:
:return:
"""
latest = comparison_objs[-1]
if latest.op == "=":
return None
def get_graph_and_vertices():
_graph = {}
_vertices = set()
for obj in comparison_objs:
if obj.op == "=":
continue
_vertices.add(obj.a)
_vertices.add(obj.b)
if obj.op == ">":
_graph.setdefault(obj.a, []).append(obj.b)
else:
_graph.setdefault(obj.b, []).append(obj.a)
return _graph, _vertices
def is_cyclic(v):
# Mark current node as visited and
# adds to recursion stack
visited[v] = True
rec_stack[v] = True
# Recur for all neighbours
# if any neighbour is visited and in
# recStack then graph is cyclic
if v in graph:
for neighbour in graph[v]:
if not visited[neighbour]:
if is_cyclic(neighbour):
return True
elif rec_stack[neighbour]:
return True
# The node needs to be poped from
# recursion stack before function ends
rec_stack[v] = False
return False
graph, vertices = get_graph_and_vertices()
visited = {k: False for k in vertices}
rec_stack = {k: False for k in vertices}
if is_cyclic(latest.a): # only need to check from the latest add, since the graph was not cyclic before
return [k for k, v in rec_stack.items() if v]
return None
@@ -0,0 +1,70 @@
import core.utils
from core.builtin_concepts import BuiltinConcepts, ErrorConcept
from core.concept import Concept
from sdp.sheerkaDataProvider import SheerkaDataProviderDuplicateKeyError
BNF_NODE_PARSER_CLASS = "parsers.BnfNodeParser_Old.BnfNodeParser"
BASE_NODE_PARSER_CLASS = "parsers.BaseNodeParser.BaseNodeParser"
class SheerkaCreateNewConcept:
"""
Manage the creation of a new concept
"""
def __init__(self, sheerka):
self.sheerka = sheerka
self.logger_name = "CreateNewConcept"
self.bnp = core.utils.get_class(BASE_NODE_PARSER_CLASS) # BaseNodeParser
def create_new_concept(self, context, concept: Concept):
"""
Adds a new concept to the system
:param context:
:param concept: DefConceptNode
:return: digest of the new concept
"""
sheerka = self.sheerka
concept.init_key()
init_bnf_ret_value = None
cache_manager = sheerka.cache_manager
if cache_manager.exists(sheerka.CONCEPTS_BY_HASH_ENTRY, concept.get_definition_hash()):
error = SheerkaDataProviderDuplicateKeyError(sheerka.CONCEPTS_BY_KEY_ENTRY + "." + concept.key,
concept)
return sheerka.ret(
self.logger_name,
False,
sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_DEFINED, body=concept),
error.args[0])
# set id before saving in db
sheerka.set_id_if_needed(concept, False)
# update the dictionary of concepts by first key
init_ret_value = self.bnp.get_concepts_by_first_keyword(context, [concept], True)
if not init_ret_value.status:
return sheerka.ret(self.logger_name, False, ErrorConcept(init_ret_value.value))
concepts_by_first_keyword = init_ret_value.body
# update resolved dictionary
init_ret_value = self.bnp.resolve_concepts_by_first_keyword(context, concepts_by_first_keyword)
if not init_ret_value.status:
return sheerka.ret(self.logger_name, False, ErrorConcept(init_ret_value.value))
resolved_concepts_by_first_keyword = init_ret_value.body
concept.freeze_definition_hash()
cache_manager.add_concept(concept)
cache_manager.put(sheerka.CONCEPTS_BY_FIRST_KEYWORD_ENTRY, False, concepts_by_first_keyword)
cache_manager.put(sheerka.RESOLVED_CONCEPTS_BY_FIRST_KEYWORD_ENTRY, False, resolved_concepts_by_first_keyword)
if concept.bnf and init_bnf_ret_value is not None and init_bnf_ret_value.status:
sheerka.cache_manager.clear(sheerka.CONCEPTS_GRAMMARS_ENTRY)
# process the return if needed
ret = sheerka.ret(self.logger_name, True, sheerka.new(BuiltinConcepts.NEW_CONCEPT, body=concept))
return ret
+98
View File
@@ -0,0 +1,98 @@
import os
import pprint
from core.builtin_concepts import BuiltinConcepts
from core.concept import Concept
from core.sheerka.ExecutionContext import ExecutionContext
from sdp.sheerkaDataProvider import SheerkaDataProvider, Event
def get_pp():
rows, columns = os.popen('stty size', 'r').read().split()
pp = pprint.PrettyPrinter(width=columns, compact=True)
return pp
class SheerkaDump:
def __init__(self, sheerka):
self.sheerka = sheerka
def dump_concepts(self):
lst = self.sheerka.sdp.list(self.sheerka.CONCEPTS_BY_ID_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_desc(self, *concept_names, eval=False):
first = True
event = Event(f"Dumping description", "")
context = ExecutionContext("dump_desc", event, self.sheerka)
for concept_name in concept_names:
if isinstance(concept_name, Concept):
concepts = concept_name
else:
concepts = self.sheerka.get_by_key(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 eval:
evaluated = self.sheerka.evaluate_concept(context, c)
value = evaluated.body if evaluated.key == c.key else evaluated
if not first:
self.sheerka.log.info("")
self.sheerka.log.info(f"name : {c.name}")
self.sheerka.log.info(f"key : {c.key}")
self.sheerka.log.info(f"bnf : {c.metadata.definition}")
self.sheerka.log.info(f"body : {c.metadata.body}")
self.sheerka.log.info(f"where : {c.metadata.where}")
if eval:
self.sheerka.log.info(f"value : {value}")
for v in c.values:
self.sheerka.log.info(f"{v}: {c.get_value(v)}")
else:
self.sheerka.log.info("No property")
self.sheerka.log.info(f"digest : {c.get_origin()}")
if self.sheerka.isaset(context, c):
items = self.sheerka.get_set_elements(context, c)
self.sheerka.log.info(f"elements : {items}")
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.result:
self.sheerka.log.info(h)
count += 1
h = next(history)
except StopIteration:
break
page_count += 1
def dump_state(self):
snapshot = self.sheerka.sdp.get_snapshot(SheerkaDataProvider.HeadFile)
state = self.sheerka.sdp.load_state(snapshot)
self.sheerka.log.info(get_pp().pformat(state.data))
@@ -0,0 +1,323 @@
from core.builtin_concepts import BuiltinConcepts
from core.builtin_helpers import expect_one, only_successful
from core.concept import Concept, DoNotResolve, ConceptParts, InfiniteRecursionResolved
CONCEPT_EVALUATION_STEPS = [
BuiltinConcepts.BEFORE_EVALUATION,
BuiltinConcepts.EVALUATION,
BuiltinConcepts.AFTER_EVALUATION]
class SheerkaEvaluateConcept:
def __init__(self, sheerka):
self.sheerka = sheerka
self.logger_name = "EvaluateConcept"
@staticmethod
def infinite_recursion_detected(context, concept):
if concept is None:
return False
parent = context.get_parent()
while parent is not None:
if parent.who == context.who and parent.obj == concept:
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
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.desc == context.desc:
body = parent.obj.metadata.body
try:
return self.sheerka.ret(self.logger_name, True, InfiniteRecursionResolved(eval(body)))
except Exception:
pass
concepts_found.add(parent.obj)
parent = parent.get_parent()
return self.sheerka.ret(
self.logger_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:
:param logger:
: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)
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.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)
only_success = only_successful(sub_context, res)
concept.compiled[part_key] = only_success.body.body if is_only_successful(only_success) else res
sub_context.add_values(return_values=res)
for var_name, default_value in concept.metadata.variables:
if var_name in concept.compiled:
continue
if default_value is None or not isinstance(default_value, str):
continue
if default_value.strip() == "":
concept.compiled[var_name] = DoNotResolve(default_value)
else:
with context.push(desc=f"Initializing *compiled* for property {var_name}") as sub_context:
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(sub_context, to_parse, steps)
only_success = only_successful(sub_context, res)
concept.compiled[var_name] = only_success.body.body if is_only_successful(only_success) else res
sub_context.add_values(return_values=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):
if isinstance(to_resolve, DoNotResolve):
return to_resolve.value
# manage infinite loop
if self.infinite_recursion_detected(context, current_concept):
with context.push(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
desc = f"Evaluating {current_prop} (concept={current_concept})"
context.log(desc, self.logger_name)
with context.push(desc=desc, obj=current_concept) as sub_context:
if force_evaluation:
sub_context.local_hints.add(BuiltinConcepts.EVAL_BODY_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 evaluated.key == to_resolve.key:
return evaluated
else:
error = evaluated
# otherwise, execute all return values to find out what is the value
else:
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)
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):
"""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)
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)
if self.sheerka.isinstance(r, BuiltinConcepts.CONCEPT_EVAL_ERROR):
return r
res.append(r)
return res
def evaluate_concept(self, context, concept: Concept):
"""
Evaluation a concept
It means that if the where clause is True, will evaluate the body
:param context:
:param concept:
:return: value of the evaluation or error
"""
if concept.metadata.is_evaluated:
return concept
self.initialize_concept_asts(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 = self.choose_metadata_to_eval(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]
if isinstance(prop_ast, list):
# Do not send the current concept for the properties
resolved = self.resolve_list(context, prop_ast, var_name, None, True)
else:
# Do not send the current concept for the properties
resolved = self.resolve(context, prop_ast, var_name, None, True)
if isinstance(resolved, Concept) and not 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(context, concept.body):
continue
if part_key in concept.compiled and concept.compiled[part_key] is not None:
metadata_ast = concept.compiled[part_key]
# if part_key is PRE, POST or WHERE, the concepts need to be evaluated
# otherwise the predicates cannot be resolved
force_concept_eval = False if part_key == ConceptParts.BODY else True
resolved = self.resolve(context, metadata_ast, part_key, concept, force_concept_eval)
if isinstance(resolved, Concept) and not context.sheerka.is_success(resolved):
return resolved
else:
concept.set_value(part_key, self.get_infinite_recursion_resolution(resolved) or resolved)
#
# TODO : Validate the PRE condition
#
# validate where clause
if ConceptParts.WHERE in concept.values:
where_value = concept.get_value(ConceptParts.WHERE)
if not (where_value is None or self.sheerka.objvalue(where_value)):
return self.sheerka.new(BuiltinConcepts.WHERE_CLAUSE_FAILED, body=concept)
#
# TODO : Validate the POST condition
#
concept.init_key() # only does it if needed
concept.metadata.is_evaluated = "body" in all_metadata_to_eval
return concept
def choose_metadata_to_eval(self, context, concept):
if context.in_context(BuiltinConcepts.EVAL_BODY_REQUESTED):
return ["pre", "post", "variables", "body", "where"]
metadata = ["pre", "post"]
if context.in_context(BuiltinConcepts.EVAL_WHERE_REQUESTED) or concept.metadata.need_validation:
needed = self.needed_metadata(concept, ConceptParts.WHERE)
for e in needed:
if e not in metadata:
metadata.append(e)
if "where" not in metadata:
metadata.append("where")
return metadata
def needed_metadata(self, concept, metadata):
"""
Tries to find out if the evaluation of the body is necessary
It's a very basic approach that will need to be improved
:param concept:
:param metadata:
:return:
"""
if metadata not in concept.compiled:
return []
return_values = concept.compiled[metadata]
if not isinstance(return_values, list):
return []
needed = []
for return_value in return_values:
if not self.sheerka.isinstance(return_value, BuiltinConcepts.RETURN_VALUE):
continue
if not return_value.status:
continue
if not self.sheerka.isinstance(return_value.body, BuiltinConcepts.PARSER_RESULT):
continue
if not isinstance(return_value.body.source, str):
continue
for var_name in (p[0] for p in concept.metadata.variables):
if var_name in return_value.body.source:
needed.append("variables")
break
if "self" in return_value.body.source:
needed.append("body")
return needed
+247
View File
@@ -0,0 +1,247 @@
import core.utils
from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept
NO_MATCH = "** No Match **"
class SheerkaExecute:
"""
Manage the execution of a process flow
"""
def __init__(self, sheerka):
self.sheerka = sheerka
def call_parsers(self, execution_context, return_values):
# 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()]
instantiated_parsers = self.preprocess(execution_context, instantiated_parsers)
grouped_parsers = {}
for parser in [p for p in instantiated_parsers if p.enabled]:
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]:
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(f"Parsing {debug_text}")
with execution_context.push(desc=f"Parsing using {parser.name}",
logger=parser.verbose_log) 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):
# if a ParserResultConcept is returned, it will be used by the parsers
# of the following groups
to_process.append(r)
if r.status:
stop_processing = True
else:
res.parents = [return_value]
result.append(res)
if self.sheerka.isinstance(res.body, BuiltinConcepts.PARSER_RESULT):
# if a ParserResultConcept is returned, it will be used by the parsers
# of the following groups
to_process.append(res)
if res.status:
stop_processing = True
sub_context.add_values(return_values=res)
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):
# return_values must be a list
if not isinstance(return_values, list):
return_values = [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(execution_context, instantiated_evaluators)
for evaluator in [e for e in instantiated_evaluators if e.enabled and process_step in e.steps]:
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(execution_context, evaluator.__class__()) # fresh copy
sub_context_desc = f"Evaluating using {evaluator.name} ({priority=})"
with iteration_context.push(desc=sub_context_desc, logger=evaluator.verbose_log) 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):
# init the evaluator is possible
if hasattr(evaluator, "init_evaluator") and not evaluator.is_initialized:
evaluator.init_evaluator(sub_context, original_items)
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
return return_values
def execute(self, execution_context, return_values, execution_steps):
"""
Executes process for all initial contexts
:param execution_context:
:param return_values:
:param execution_steps:
: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:
if step == BuiltinConcepts.PARSING:
return_values = self.call_parsers(sub_context, return_values)
else:
return_values = self.call_evaluators(sub_context, return_values, step)
if copy != return_values:
sub_context.log_result(return_values)
sub_context.add_values(return_values=return_values)
return return_values
def preprocess(self, context, parsers_or_evaluators):
if not context.preprocess:
return parsers_or_evaluators
if not hasattr(parsers_or_evaluators, "__iter__"):
single_one = True
parsers_or_evaluators = [parsers_or_evaluators]
else:
single_one = False
for preprocess in context.preprocess:
for e in parsers_or_evaluators:
if self.matches(e.name, preprocess.get_value("name")):
for var_name in preprocess.values:
if var_name == "name":
continue
if hasattr(e, var_name):
setattr(e, var_name, preprocess.get_value(var_name))
return parsers_or_evaluators[0] if single_one else parsers_or_evaluators
@staticmethod
def matches(parser_or_evaluator_name, preprocessor_name):
if preprocessor_name.endswith("*"):
return parser_or_evaluator_name.startswith(preprocessor_name[:-1])
else:
return parser_or_evaluator_name == preprocessor_name
@@ -0,0 +1,63 @@
from collections import namedtuple
from sdp.sheerkaDataProvider import Event
hist = namedtuple("HistoryTest", "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"History(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
self._status = self.result.get_status() if self.result else None
return self._status
class SheerkaHistoryManager:
def __init__(self, sheerka):
self.sheerka = sheerka
def history(self, depth, start):
"""
Load history
:param depth: number of items
:param start:
:return:
"""
events = list(self.sheerka.sdp.load_events(depth, start))
for event in events:
try:
result = self.sheerka.sdp.load_result(event.get_digest())
except (IOError, KeyError):
result = None
yield History(event, result)
@@ -0,0 +1,41 @@
from core.builtin_concepts import BuiltinConcepts
class SheerkaModifyConcept:
def __init__(self, sheerka):
self.sheerka = sheerka
self.logger_name = "ModifyConcept"
def modify_concept(self, context, concept):
old_version = self.sheerka.get_by_id(concept.id)
if old_version is None:
# nothing found in cache
return self.sheerka.ret(
self.logger_name, False,
self.sheerka.new(
BuiltinConcepts.UNKNOWN_CONCEPT,
body=[("key", concept.key), ("id", concept.id)]))
if not self.sheerka.is_success(old_version) and concept.key != old_version.key:
# an error concept is returned
return self.sheerka.ret(
self.logger_name, False,
old_version)
if old_version == concept:
# the concept is not modified
return self.sheerka.ret(
self.logger_name, False,
self.sheerka.new(
BuiltinConcepts.CONCEPT_ALREADY_DEFINED,
body=concept))
self.sheerka.cache_manager.update_concept(old_version, concept)
# TODO : update concept by first keyword
# TODO : update resolved by first keyword
# TODO : update concepts grammars
ret = self.sheerka.ret(self.logger_name, True, self.sheerka.new(BuiltinConcepts.NEW_CONCEPT, body=concept))
return ret
@@ -0,0 +1,232 @@
import core.builtin_helpers
from core.ast.nodes import python_to_concept
from core.builtin_concepts import BuiltinConcepts
from core.concept import Concept, ConceptParts
GROUP_PREFIX = 'All_'
class SheerkaSetsManager:
def __init__(self, sheerka):
self.sheerka = sheerka
self.logger_name = "SetsManager"
def set_isa(self, context, concept, concept_set):
"""
Defines that concept a is b is another concept
:param context:
:param concept:
:param concept_set:
:return:
"""
context.log(f"Setting concept {concept} is a {concept_set}", who=self.logger_name)
if BuiltinConcepts.ISA in concept.metadata.props and concept_set in concept.metadata.props[BuiltinConcepts.ISA]:
return self.sheerka.ret(
self.logger_name,
False,
self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_IN_SET, body=concept, concept_set=concept_set))
concept.add_prop(BuiltinConcepts.ISA, concept_set)
res = self.sheerka.modify_concept(context, concept)
if not res.status:
return res
return self.add_concept_to_set(context, concept, concept_set)
def add_concept_to_set(self, context, concept, concept_set):
"""
Add an entry in sdp to tell that concept isa concept_set
:param context:
:param concept:
:param concept_set:
:return:
"""
context.log(f"Adding concept {concept} to set {concept_set}", who=self.logger_name)
assert concept.id
assert concept_set.id
set_elements = self.sheerka.cache_manager.get(self.sheerka.CONCEPTS_GROUPS_ENTRY, concept_set.id)
if set_elements and concept.id in set_elements:
return self.sheerka.ret(
self.logger_name,
False,
self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_IN_SET, body=concept, concept_set=concept_set))
self.sheerka.cache_manager.put(self.sheerka.CONCEPTS_GROUPS_ENTRY, concept_set.id, concept.id)
return self.sheerka.ret(self.logger_name, True, self.sheerka.new(BuiltinConcepts.SUCCESS))
def add_concepts_to_set(self, context, concepts, concept_set):
"""Adding multiple concepts at the same time"""
context.log(f"Adding concepts {concepts} to set {concept_set}", who=self.logger_name)
already_in_set = []
for concept in concepts:
res = self.add_concept_to_set(context, concept, concept_set)
if self.sheerka.isinstance(res.body, BuiltinConcepts.CONCEPT_ALREADY_IN_SET):
already_in_set.append(res.body.body)
if already_in_set:
body = self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_IN_SET,
body=already_in_set,
concept_set=concept_set)
else:
body = self.sheerka.new(BuiltinConcepts.SUCCESS)
return self.sheerka.ret(self.logger_name, len(already_in_set) != len(concepts), body)
def get_set_elements(self, context, concept):
"""
Concept is supposed to be a set
Returns all elements if the set
:param context:
:param concept:
:return:
"""
def _get_set_elements(sub_concept):
if not self.isaset(context, sub_concept):
return self.sheerka.new(BuiltinConcepts.NOT_A_SET, body=concept)
# first, try to see if sub_context has it's own group entry
ids = self.sheerka.cache_manager.get(self.sheerka.CONCEPTS_GROUPS_ENTRY, sub_concept.id)
concepts = self._get_concepts(context, ids, True)
# aggregate with en entries from its body
sub_concept = core.builtin_helpers.ensure_evaluated(context, sub_concept)
if not self.sheerka.is_success(sub_concept):
return sub_concept
if self.isaset(context, sub_concept.body):
other_concepts = _get_set_elements(sub_concept.body)
if not self.sheerka.is_success(other_concepts):
return other_concepts
concepts.extend(other_concepts)
# apply the where clause if any
if sub_concept.metadata.where:
new_condition = self._validate_where_clause(sub_concept)
if not new_condition:
return self.sheerka.new(BuiltinConcepts.WHERE_CLAUSE_FAILED, body=sub_concept)
# This methods sucks, but I don't have enough tools (like proper AST manipulation functions)
# to do it properly now. It will be enhanced later
globals_ = {"xx__concepts__xx": concepts, "sheerka": self.sheerka}
locals_ = {}
exec(new_condition, globals_, locals_)
concepts = locals_["result"]
return concepts
return _get_set_elements(concept)
def isinset(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")
if not (isinstance(a, Concept) and isinstance(b, Concept)):
return False
# TODO, first check the 'isa' property of a
if not (a.id and b.id):
return False
group_elements = self.sheerka.cache_manager.get(self.sheerka.CONCEPTS_GROUPS_ENTRY, b.id)
return group_elements and a.id in group_elements
def isa(self, a, b):
if BuiltinConcepts.ISA not in a.metadata.props:
return False
for c in a.metadata.props[BuiltinConcepts.ISA]:
if c == b:
return True
if self.isa(c, b):
return True
return False
def isaset(self, context, concept):
"""
True if exists All_<concept_id> in sdp or if concept references to a concept that has all_<concept_id>
:param context:
:param concept:
:return:
"""
""""""
if not (isinstance(concept, Concept) and concept.id):
return False
# check if it has a group
# TODO: use cache instead of directly requesting sdp
if self.sheerka.cache_manager.get(self.sheerka.CONCEPTS_GROUPS_ENTRY, concept.id):
return True
# it may be a concept that references a set
concept = core.builtin_helpers.ensure_evaluated(context, concept)
if not context.sheerka.is_success(concept):
return False
return self.isaset(context, concept.body)
def _validate_where_clause(self, concept):
python_parser_result = [r for r in concept.compiled[ConceptParts.WHERE] if r.who == "parsers.Python"]
if not python_parser_result or not python_parser_result[0].status:
return None
ast_ = python_parser_result[0].body.body.ast_
ast_as_concepts = python_to_concept(ast_)
names = core.builtin_helpers.get_names(self.sheerka, ast_as_concepts)
if len(names) != 1 or names[0] != concept.metadata.body:
return None
condition = concept.metadata.where.replace(concept.metadata.body, "sheerka.objvalue(x)")
expression = f"""
result=[]
for x in xx__concepts__xx:
try:
if {condition}:
result.append(x)
except Exception:
pass
"""
return expression
def _get_concepts(self, context, ids, evaluate):
"""
Gets concepts from a list of concepts ids
:param ids:
:param evaluate: if True, all the elements are evaluated before returned
:return:
"""
if not ids:
return []
if not evaluate:
return [self.sheerka.get_by_id(element_id) for element_id in ids]
result = []
with context.push(desc=f"Evaluating concepts of a set") as sub_context:
sub_context.add_inputs(ids=ids)
sub_context.local_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
for element_id in ids:
concept = self.sheerka.get_by_id(element_id)
evaluated = self.sheerka.evaluate_concept(sub_context, concept)
result.append(evaluated)
sub_context.add_inputs(return_value=result)
return result
@@ -0,0 +1,47 @@
from dataclasses import dataclass
from typing import List
from core.sheerka.services.sheerka_service import ServiceObj
@dataclass
class Variable(ServiceObj):
"""
Variable to store
"""
who: str # who is the modifier
key: str # key of the variable
value: object # value
parents: List[str] # previous references of the variable (Note that there should be only one parent)
def get_key(self):
return f"{self.who}|{self.key}"
class SheerkaVariableManager:
def __init__(self, sheerka):
self.sheerka = sheerka
def record(self, context, who, key, value):
"""
:param context:
:param who: entity that owns the key (acts as a namespace)
:param key:
:param value:
:return:
"""
variable = Variable(context.event.get_digest(), who, key, value, None)
self.sheerka.cache_manager.put(self.sheerka.VARIABLES_ENTRY, variable.get_key(), variable)
def load(self, who, key):
variable = self.sheerka.cache_manager.get(self.sheerka.VARIABLES_ENTRY, who + "|" + key)
if variable is None:
return None
return variable.value
def delete(self, context, who, key):
self.sheerka.cache_manager.delete(self.sheerka.VARIABLES_ENTRY, who + "|" + key)
@@ -0,0 +1,14 @@
from dataclasses import dataclass
@dataclass
class ServiceObj:
event_id: str # event where the object is created / modified
class BaseService:
"""
Base class for services
"""
def __init__(self, sheerka):
self.sheerka = sheerka