Implemented a first and basic version of a Rete rule engine

This commit is contained in:
2021-02-09 16:06:32 +01:00
parent 821dbed189
commit a2a8d5c5e5
110 changed files with 7301 additions and 1654 deletions
+36 -20
View File
@@ -3,7 +3,7 @@ import time
from os import path
from core.builtin_concepts_ids import BuiltinConcepts, BuiltinContainers
from core.builtin_helpers import ensure_concept
from core.builtin_helpers import ensure_concept_or_rule
from core.concept import Concept
from core.sheerka.services.SheerkaMemory import SheerkaMemory
from core.sheerka.services.sheerka_service import BaseService
@@ -28,6 +28,7 @@ class SheerkaAdmin(BaseService):
self.sheerka.bind_service_method(self.extended_isinstance, False)
self.sheerka.bind_service_method(self.is_container, False)
self.sheerka.bind_service_method(self.format_rules, False)
self.sheerka.bind_service_method(self.exec_rules, False)
self.sheerka.bind_service_method(self.admin_push_ontology, True, as_name="push_ontology")
self.sheerka.bind_service_method(self.admin_pop_ontology, True, as_name="pop_ontology")
self.sheerka.bind_service_method(self.ontologies, False)
@@ -127,24 +128,36 @@ class SheerkaAdmin(BaseService):
concepts = sorted(self.sheerka.om.list(self.sheerka.CONCEPTS_BY_ID_ENTRY), key=lambda item: int(item.id))
return self.sheerka.new(BuiltinConcepts.TO_LIST, body=concepts)
def desc(self, *concepts):
ensure_concept(*concepts)
def desc(self, *items):
ensure_concept_or_rule(*items)
res = []
for c in concepts:
bag = {
"id": c.id,
"name": c.name,
"key": c.key,
"definition": c.get_metadata().definition,
"type": c.get_metadata().definition_type,
"body": c.get_metadata().body,
"where": c.get_metadata().where,
"pre": c.get_metadata().pre,
"post": c.get_metadata().post,
"ret": c.get_metadata().ret,
"vars": c.get_metadata().variables,
"props": c.get_metadata().props,
}
for item in items:
if isinstance(item, Concept):
bag = {
"id": item.id,
"name": item.name,
"key": item.key,
"definition": item.get_metadata().definition,
"type": item.get_metadata().definition_type,
"body": item.get_metadata().body,
"where": item.get_metadata().where,
"pre": item.get_metadata().pre,
"post": item.get_metadata().post,
"ret": item.get_metadata().ret,
"vars": item.get_metadata().variables,
"props": item.get_metadata().props,
}
else:
bag = {
"id": item.id,
"name": item.metadata.name,
"type": item.metadata.action_type,
"predicate": item.metadata.predicate,
"action": item.metadata.action,
"priority": item.priority,
"compiled": item.metadata.is_compiled,
"enabled": item.metadata.is_enabled,
}
res.append(self.sheerka.new(BuiltinConcepts.TO_DICT, body=bag))
return res[0] if len(res) == 1 else self.sheerka.new(BuiltinConcepts.TO_LIST, body=res)
@@ -152,6 +165,9 @@ class SheerkaAdmin(BaseService):
def format_rules(self):
return self.sheerka.new(BuiltinConcepts.TO_LIST, items=self.sheerka.get_format_rules())
def exec_rules(self):
return self.sheerka.new(BuiltinConcepts.TO_LIST, items=self.sheerka.get_exec_rules())
def extended_isinstance(self, a, b):
"""
switch between sheerka.isinstance and builtin.isinstance
@@ -180,8 +196,8 @@ class SheerkaAdmin(BaseService):
def admin_push_ontology(self, context, name):
return self.sheerka.push_ontology(context, name, False)
def admin_pop_ontology(self):
return self.sheerka.pop_ontology()
def admin_pop_ontology(self, context):
return self.sheerka.pop_ontology(context)
def ontologies(self):
ontologies = self.sheerka.om.ontologies_names
@@ -38,7 +38,7 @@ class SheerkaComparisonManager(BaseService):
DEFAULT_COMPARISON_VALUE = 1
def __init__(self, sheerka):
super().__init__(sheerka)
super().__init__(sheerka, order=14)
def initialize(self):
cache = ListCache().auto_configure(self.COMPARISON_ENTRY)
@@ -12,8 +12,7 @@ from core.builtin_concepts_ids import BuiltinConcepts, AllBuiltinConcepts, Built
from core.builtin_helpers import ensure_concept, ensure_bnf
from core.concept import Concept, DEFINITION_TYPE_DEF, DEFINITION_TYPE_BNF, freeze_concept_attrs, ConceptMetadata, \
VARIABLE_PREFIX
from core.error import ErrorObj
from core.global_symbols import EVENT_CONCEPT_CREATED, NotInit, NotFound
from core.global_symbols import EVENT_CONCEPT_CREATED, NotInit, NotFound, ErrorObj, EVENT_CONCEPT_DELETED
from core.sheerka.services.sheerka_service import BaseService
from core.tokenizer import Tokenizer, TokenKind
from sdp.sheerkaDataProvider import SheerkaDataProviderDuplicateKeyError
@@ -100,7 +99,7 @@ class SheerkaConceptManager(BaseService):
RESOLVED_CONCEPTS_BY_FIRST_KEYWORD_ENTRY = "ConceptManager:Resolved_Concepts_By_First_Keyword"
def __init__(self, sheerka):
super().__init__(sheerka)
super().__init__(sheerka, order=11)
self.forbidden_meta = {"is_builtin", "key", "id", "props", "variables"}
self.allowed_meta = {attr for attr in vars(ConceptMetadata) if
not attr.startswith("_") and attr not in self.forbidden_meta}
@@ -147,7 +146,7 @@ class SheerkaConceptManager(BaseService):
self.sheerka.om.put(self.sheerka.OBJECTS_IDS_ENTRY, self.USER_CONCEPTS_IDS, 1000)
# initialize the dictionary of first tokens
self.sheerka.om.get(self.CONCEPTS_BY_FIRST_KEYWORD_ENTRY, None) # to init the cache with the values from sdp
self.sheerka.om.get(self.CONCEPTS_BY_FIRST_KEYWORD_ENTRY, None) # to init the cache with the values from sdp
concepts_by_first_keyword = self.sheerka.om.current_cache_manager().copy(self.CONCEPTS_BY_FIRST_KEYWORD_ENTRY)
res = self.resolve_concepts_by_first_keyword(context, concepts_by_first_keyword)
self.sheerka.om.put(self.RESOLVED_CONCEPTS_BY_FIRST_KEYWORD_ENTRY, False, res.body)
@@ -353,6 +352,8 @@ class SheerkaConceptManager(BaseService):
:param concept:
:return:
"""
# TODO : resolve concept first
sheerka = context.sheerka
refs = self.sheerka.om.get(self.CONCEPTS_REFERENCES_ENTRY, concept.id)
if refs is not NotFound:
@@ -361,6 +362,7 @@ class SheerkaConceptManager(BaseService):
try:
sheerka.om.remove_concept(concept)
sheerka.publish(context, EVENT_CONCEPT_DELETED, concept)
return sheerka.ret(self.NAME, True, sheerka.new(BuiltinConcepts.SUCCESS))
except ConceptNotFound as ex:
return sheerka.ret(self.NAME, False, sheerka.err(ex))
@@ -91,6 +91,14 @@ class BaseDebugLogger:
BaseDebugLogger.ids[hint] = 0
return BaseDebugLogger.ids[hint]
@staticmethod
def current_id(hint):
if hint in BaseDebugLogger.ids:
return BaseDebugLogger.ids[hint]
else:
BaseDebugLogger.ids[hint] = 0
return 0
def __init__(self, debug_manager, context, who, method_name, debug_id):
pass
@@ -276,7 +284,7 @@ class SheerkaDebugManager(BaseService):
children_activation_regex = re.compile(r"(\d+)\+")
def __init__(self, sheerka):
super().__init__(sheerka)
super().__init__(sheerka, order=1)
self.activated = False # is debug activated
self.explicit = False # No need to activate context debug when debug mode is on # to remove ?
self.context_cache = set() # debug for specific context # to remove ?
@@ -295,16 +303,6 @@ class SheerkaDebugManager(BaseService):
]
def initialize(self):
# TO REMOVE ???
self.sheerka.bind_service_method(self.set_explicit, True)
self.sheerka.bind_service_method(self.activate_debug_for, True)
self.sheerka.bind_service_method(self.deactivate_debug_for, True)
self.sheerka.bind_service_method(self.debug_activated, False)
self.sheerka.bind_service_method(self.debug_activated_for, False)
self.sheerka.bind_service_method(self.get_context_debug_mode, False)
self.sheerka.bind_service_method(self.debug_rule_activated, False)
self.sheerka.bind_service_method(self.debug, False, visible=False)
self.sheerka.bind_service_method(self.set_debug, True)
self.sheerka.bind_service_method(self.inspect, False)
self.sheerka.bind_service_method(self.get_debugger, False)
@@ -341,79 +339,13 @@ class SheerkaDebugManager(BaseService):
self.sheerka.record_var(context, self.NAME, "activated", self.activated)
return self.sheerka.ret(SheerkaDebugManager.NAME, True, self.sheerka.new(BuiltinConcepts.SUCCESS))
def set_explicit(self, context, value=True):
self.explicit = value
self.sheerka.record_var(context, self.NAME, "explicit", self.explicit)
return self.sheerka.ret(SheerkaDebugManager.NAME, True, self.sheerka.new(BuiltinConcepts.SUCCESS))
def activate_debug_for(self, context, debug_id, children=False):
"""
:param context:
:param debug_id: if debug_id is str, activate variable cache, context_cache otherwise
:param children:
:return:
"""
# preprocess
if isinstance(debug_id, str) and (m := self.children_activation_regex.match(debug_id)):
debug_id = int(m.group(1))
children = True
if isinstance(debug_id, str):
self.variable_cache.add(debug_id)
self.sheerka.record_var(context, self.NAME, "variable_cache", self.variable_cache)
else:
self.context_cache.add(debug_id)
if children:
self.context_cache.add(str(debug_id) + "+")
self.sheerka.record_var(context, self.NAME, "context_cache", self.context_cache)
return self.sheerka.ret(SheerkaDebugManager.NAME, True, self.sheerka.new(BuiltinConcepts.SUCCESS))
def deactivate_debug_for(self, context, debug_id, children=False):
if isinstance(debug_id, str):
self.variable_cache.discard(debug_id)
self.sheerka.record_var(context, self.NAME, "variable_cache", self.variable_cache)
else:
self.context_cache.discard(debug_id)
if children:
self.context_cache.discard(str(debug_id) + "+")
self.sheerka.record_var(context, self.NAME, "context_cache", self.context_cache)
return self.sheerka.ret(SheerkaDebugManager.NAME, True, self.sheerka.new(BuiltinConcepts.SUCCESS))
def debug_activated(self):
return self.activated
def debug_activated_for(self, debug_id):
if not self.activated:
return None
return debug_id in self.variable_cache
def debug_rule_activated(self, rule_id, context_id):
"""
:param rule_id:
:param context_id:
:return:
"""
key = f"{rule_id}|{context_id}"
return key in self.rules_cache
def get_context_debug_mode(self, context_id):
if not self.activated:
return None, None
debug_for_children = "protected" if str(context_id) + "+" in self.context_cache else None
debug_for_self = "private" if not self.explicit or context_id in self.context_cache else None
return debug_for_self, debug_for_children
def debug(self, *args, **kwargs):
print(*args, **kwargs)
def get_debugger(self, context, who, method_name):
def get_debugger(self, context, who, method_name, new_debug_id=True):
if self.compute_debug(context, who, method_name):
debug_id = ConsoleDebugLogger.next_id(context.event.get_digest() + str(context.id))
debug_id = ConsoleDebugLogger.next_id(context.event.get_digest() + str(context.id)) if new_debug_id \
else ConsoleDebugLogger.current_id(context.event.get_digest() + str(context.id))
return ConsoleDebugLogger(self, context, who, method_name, debug_id)
return NullDebugLogger()
@@ -11,7 +11,8 @@ from core.sheerka.services.sheerka_service import BaseService
from core.tokenizer import Tokenizer
from core.utils import unstr_concept
from parsers.BaseNodeParser import ConceptNode
from parsers.ExpressionParser import ExpressionParser, TrueifyVisitor
from parsers.ExpressionParser import ExpressionParser
from parsers.expressions import TrueifyVisitor
CONCEPT_EVALUATION_STEPS = [
BuiltinConcepts.BEFORE_EVALUATION,
@@ -1,7 +1,9 @@
from core.builtin_concepts import BuiltinConcepts
from core.builtin_helpers import expect_one
from core.global_symbols import EVENT_RULE_CREATED, EVENT_RULE_DELETED, EVENT_RULE_ID_DELETED
from core.sheerka.services.sheerka_service import BaseService
from evaluators.ConceptEvaluator import ConceptEvaluator
from sheerkarete.network import ReteNetwork
DISABLED_RULES = "#disabled#"
LOW_PRIORITY_RULES = "#low_priority#"
@@ -11,12 +13,18 @@ class SheerkaEvaluateRules(BaseService):
NAME = "EvaluateRules"
def __init__(self, sheerka):
super().__init__(sheerka)
# order must be before RuleManager because of event subscription
super().__init__(sheerka, 4)
self.evaluators_by_name = None
self.network = ReteNetwork()
def initialize(self):
self.sheerka.bind_service_method(self.evaluate_format_rules, False)
self.sheerka.bind_service_method(self.evaluate_format_rules, False, visible=False)
self.sheerka.bind_service_method(self.evaluate_exec_rules, False, visible=False)
self.reset_evaluators()
self.sheerka.subscribe(EVENT_RULE_CREATED, self.on_rule_created)
self.sheerka.subscribe(EVENT_RULE_DELETED, self.on_rule_deleted)
self.sheerka.subscribe(EVENT_RULE_ID_DELETED, self.on_rule_deleted)
def reset_evaluators(self):
# instantiate evaluators, once for all, only keep when it's enabled
@@ -24,6 +32,20 @@ class SheerkaEvaluateRules(BaseService):
evaluators = [e for e in evaluators if e.enabled]
self.evaluators_by_name = {e.short_name: e for e in evaluators}
def evaluate_exec_rules(self, context, return_values):
# self.network.add_obj("__rets", return_values)
for ret in return_values:
self.network.add_obj("__ret", ret)
results = [] # list of return values, for activated rules
for match in self.network.matches:
for rule in match.pnode.rules:
body = context.sheerka.new(BuiltinConcepts.RULE_EVALUATION_RESULT, rule=rule)
return_value = context.sheerka.ret(self.NAME, True, body)
results.append(return_value)
return results
def evaluate_format_rules(self, context, bag, disabled):
return self.evaluate_rules(context, self.sheerka.get_format_rules(), bag, disabled)
@@ -37,7 +59,7 @@ class SheerkaEvaluateRules(BaseService):
:param disabled: disabled rules (because they have already been fired or whatever)
:return: { True : list of success, False :list of failed, '#disabled"': list of disabled...}
"""
with context.push(BuiltinConcepts.EVALUATING_RULES, bag, desc="Evaluating rules...") as sub_context:
with context.push(BuiltinConcepts.RULES_EVALUATION, bag, desc="Evaluating rules...") as sub_context:
sub_context.protected_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
sub_context.protected_hints.add(BuiltinConcepts.EVAL_WHERE_REQUESTED)
sub_context.protected_hints.add(BuiltinConcepts.EVAL_UNTIL_SUCCESS_REQUESTED)
@@ -81,7 +103,7 @@ class SheerkaEvaluateRules(BaseService):
"""
results = []
for rule_predicate in rule.compiled_predicate:
for rule_predicate in rule.compiled_predicates:
if rule_predicate.source in bag:
# simple case where the rule is an item of the bag. No need of complicate evaluation
@@ -94,15 +116,44 @@ class SheerkaEvaluateRules(BaseService):
rule_predicate.concept.get_metadata().is_evaluated = False
evaluator = self.evaluators_by_name[rule_predicate.evaluator]
results.append(evaluator.eval(context, rule_predicate.predicate))
res = evaluator.eval(context, rule_predicate.predicate)
if res.status and isinstance(res.body, bool) and res.body:
# one successful value found. No need to look any further
results = [res]
break
else:
results.append(res)
debugger = context.get_debugger(SheerkaEvaluateRules.NAME, "evaluate_rule")
debugger = context.get_debugger(SheerkaEvaluateRules.NAME, "evaluate_rule", new_debug_id=False)
debugger.debug_rule(rule, results)
# if context.sheerka.debug_rule_activated(rule_id, context.id):
# context.debug(SheerkaEvaluateRules.NAME, "evaluate_rules", f"result(#{rule_id})", results)
return expect_one(context, results)
def remove_from_rete_memory(self, lst):
if lst is None:
return
for obj in lst:
self.network.remove_obj(obj)
def on_rule_created(self, context, rule):
"""
When a new rule is added to the system, update the network
"""
if rule.metadata.is_enabled and rule.rete_disjunctions:
self.network.add_rule(rule)
def on_rule_deleted(self, context, rule):
"""
When a rule is deleted from the system, remove it from the network
"""
if isinstance(rule, str):
rule = self.sheerka.get_rule_by_id(rule)
if not self.sheerka.is_known(rule):
return
self.network.remove_rule(rule)
@staticmethod
def get_debug_format(result):
"""
@@ -11,7 +11,7 @@ class SheerkaEventManager(BaseService):
NAME = "EventManager"
def __init__(self, sheerka):
super().__init__(sheerka)
super().__init__(sheerka, order=2)
self._lock = RLock()
self.subscribers = {}
+65 -4
View File
@@ -1,5 +1,4 @@
import core.utils
from cache.Cache import Cache
from cache.FastCache import FastCache
from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept
from core.global_symbols import NotFound
@@ -16,6 +15,8 @@ EVALUATOR_STEPS = [
BuiltinConcepts.BEFORE_RENDERING,
BuiltinConcepts.RENDERING,
BuiltinConcepts.AFTER_RENDERING,
BuiltinConcepts.BEFORE_RULES_EVALUATION,
BuiltinConcepts.AFTER_RULES_EVALUATION,
]
@@ -120,6 +121,10 @@ class ParserInput:
return self.pos < self.end
def the_token_after(self, skip_whitespace=True):
"""
Returns the token after the current one
Never returns None (returns TokenKind.EOF instead)
"""
my_pos = self.pos + 1
if my_pos >= self.end:
return Token(TokenKind.EOF, "", -1, -1, -1)
@@ -167,7 +172,8 @@ class SheerkaExecute(BaseService):
PARSERS_INPUTS_ENTRY = "Execute:ParserInput" # entry for admin or internal variables
def __init__(self, sheerka):
super().__init__(sheerka)
# order must be after SheerkaEvaluateRules because of self.rules_evaluation_service
super().__init__(sheerka, order=5)
self.pi_cache = FastCache(default=lambda key: ParserInput(key), max_size=20)
self.instantiated_evaluators = None
self.evaluators_by_name = None
@@ -191,12 +197,18 @@ class SheerkaExecute(BaseService):
# Except 2 : we store the type of the parser, not its instance
self.grouped_parsers_cache = {}
self.rules_eval_service = None
def initialize(self):
self.sheerka.bind_service_method(self.execute, True)
self.sheerka.bind_service_method(self.execute, True, visible=False)
self.sheerka.bind_service_method(self.execute_rules, True, visible=False)
self.reset_registered_evaluators()
self.reset_registered_parsers()
from core.sheerka.services.SheerkaEvaluateRules import SheerkaEvaluateRules
self.rules_eval_service = self.sheerka.services[SheerkaEvaluateRules.NAME]
def reset_state(self):
self.pi_cache.clear()
@@ -347,7 +359,7 @@ class SheerkaExecute(BaseService):
if pi is NotFound: # when CacheManager.cache_only is True
pi = ParserInput(text)
self.pi_cache.put(text, pi)
return pi
return ParserInput(text, pi.tokens) # new instance, but no need to tokenize the text again
key = text or core.utils.get_text_from_tokens(tokens)
pi = ParserInput(key, tokens)
@@ -582,6 +594,55 @@ class SheerkaExecute(BaseService):
return return_values
def execute_rules(self, context, return_values, rules_steps, evaluation_steps):
""""
Executes the execution rules until no match is found
:param context:
:param return_values: input return values
:param rules_steps: steps are configurable
:param evaluation_steps: steps are configurable
:return: out return_values
"""
continue_execution = True
counter = 0
in_rete_memory = None
while continue_execution:
with context.push(BuiltinConcepts.PROCESSING, {"counter": counter}, desc=f"{counter=}") as sub_context:
# apply rule evaluation steps
for step in rules_steps:
if step == BuiltinConcepts.RULES_EVALUATION:
eval_res = self.rules_eval_service.evaluate_exec_rules(sub_context, return_values)
if not eval_res:
self.rules_eval_service.remove_from_rete_memory(return_values)
continue_execution = False
break
else:
in_rete_memory = return_values.copy()
return_values = eval_res
else:
return_values = self.call_evaluators(sub_context, return_values, step)
if not continue_execution:
break
# evaluate the result
return_values = [r.body.body.compiled_action for r in return_values]
while True:
copy = return_values[:]
for step in evaluation_steps:
return_values = self.call_evaluators(sub_context, return_values, step)
if copy == return_values[:]:
break
# evaluation is done. Remove object in Rete memory
self.rules_eval_service.remove_from_rete_memory(in_rete_memory)
counter += 1
return return_values
def undo_preprocess(self):
for item, var_name, value in self.old_values:
setattr(item, var_name, value)
@@ -7,7 +7,7 @@ class SheerkaHasAManager(BaseService):
NAME = "HasAManager"
def __init__(self, sheerka):
super().__init__(sheerka)
super().__init__(sheerka, order=22)
def initialize(self):
self.sheerka.bind_service_method(self.set_hasa, True)
@@ -15,7 +15,7 @@ class SheerkaIsAManager(BaseService):
CONCEPTS_IN_GROUPS_ENTRY = "IsAManager:Concepts_In_Groups" # cache for get_set_elements()
def __init__(self, sheerka):
super().__init__(sheerka)
super().__init__(sheerka, order=21)
def initialize(self):
self.sheerka.bind_service_method(self.set_isa, True)
+3 -4
View File
@@ -20,7 +20,7 @@ class SheerkaMemory(BaseService):
OBJECTS_ENTRY = "Memory:Objects"
def __init__(self, sheerka):
super().__init__(sheerka)
super().__init__(sheerka, order=13)
self.short_term_objects = FastCache()
self.registration = {}
@@ -39,6 +39,8 @@ class SheerkaMemory(BaseService):
cache = ListIfNeededCache().auto_configure(self.OBJECTS_ENTRY)
self.sheerka.om.register_cache(self.OBJECTS_ENTRY, cache, persist=True, use_ref=True)
self.sheerka.subscribe(EVENT_CONTEXT_DISPOSED, self.remove_context)
def reset(self):
self.short_term_objects.clear()
@@ -48,9 +50,6 @@ class SheerkaMemory(BaseService):
self.short_term_objects.clear()
self.registration.clear()
def initialize_deferred(self, context, is_first_time):
self.sheerka.subscribe(EVENT_CONTEXT_DISPOSED, self.remove_context)
def get_from_short_term_memory(self, context, key):
while True:
try:
+1
View File
@@ -31,6 +31,7 @@ class SheerkaOut(BaseService):
if valid_rules:
if len(valid_rules) > 1:
# TODO manage when too many rules
print("TODO: TOO MANY RULES !!!!!")
pass
rule = valid_rules[0]
@@ -37,11 +37,12 @@ class SheerkaResultManager(BaseService):
self.sheerka.bind_service_method(self.get_last_created_concept, False, as_name="last_created_concept")
self.sheerka.bind_service_method(self.get_last_error, False, as_name="last_err")
def initialize_deferred(self, context, is_first_time):
self.restore_values(*self.state_vars)
self.sheerka.subscribe(EVENT_USER_INPUT_EVALUATED, self.user_input_evaluated)
self.sheerka.subscribe(EVENT_CONCEPT_CREATED, self.new_concept_created)
def initialize_deferred(self, context, is_first_time):
self.restore_values(*self.state_vars)
def test_only_reset(self):
self.executions_contexts_cache.clear()
self.last_execution = None
+343 -83
View File
@@ -1,28 +1,35 @@
import operator
import re
from dataclasses import dataclass
from typing import Union
from typing import Union, Set, List
from cache.Cache import Cache
from cache.ListIfNeededCache import ListIfNeededCache
from core.builtin_concepts import BuiltinConcepts, ReturnValueConcept
from core.builtin_helpers import parse_unrecognized, only_successful, ensure_rule
from core.builtin_helpers import parse_unrecognized, is_a_question, parse_python, \
ensure_evaluated, expect_one, parse_expression
from core.concept import Concept
from core.global_symbols import EVENT_RULE_PRECEDENCE_MODIFIED, RULE_COMPARISON_CONTEXT, NotFound
from core.rule import Rule
from core.sheerka.services.sheerka_service import BaseService
from core.global_symbols import EVENT_RULE_PRECEDENCE_MODIFIED, RULE_COMPARISON_CONTEXT, NotFound, ErrorObj, \
EVENT_RULE_CREATED, EVENT_RULE_DELETED
from core.rule import Rule, ACTION_TYPE_PRINT
from core.sheerka.Sheerka import RECOGNIZED_BY_NAME, RECOGNIZED_BY_ID
from core.sheerka.services.sheerka_service import BaseService, FailedToCompileError
from core.tokenizer import Keywords, TokenKind, Token, IterParser
from core.utils import index_tokens, COLORS, get_text_from_tokens
from evaluators.ConceptEvaluator import ConceptEvaluator
from evaluators.PythonEvaluator import PythonEvaluator
from evaluators.PythonEvaluator import PythonEvaluator, Expando
from parsers.BaseNodeParser import SourceCodeWithConceptNode, ConceptNode, SourceCodeNode
from parsers.ExpressionParser import AndNode, ExpressionParser
from parsers.PythonParser import PythonNode
from sheerkarete.conditions import AndConditions
CONCEPTS_ONLY_PARSERS = ["ExactConcept", "Bnf", "Sya", "Sequence"]
identifier_regex = re.compile(r"[\w _.]+")
@dataclass
class FormatRuleError:
class FormatRuleError(ErrorObj):
pass
@@ -199,7 +206,7 @@ class FormatAstMulti(FormatAstNode):
**kwargs)
class FormatRuleParser(IterParser):
class FormatRuleActionParser(IterParser):
@staticmethod
def to_text(list_or_dict_of_tokens):
@@ -242,7 +249,7 @@ class FormatRuleParser(IterParser):
def parse(self):
"""
Parses a format rule
Parses the print part of the format rule
format ::= {variable'} | function(...) | rawtext
:return:
"""
@@ -394,7 +401,7 @@ class FormatRuleParser(IterParser):
source = get_text_from_tokens(args[0])
if len(source) > 1 and source[0] in ("'", '"') and source[-1] in ("'", '"'):
source = source[1:-1]
parser = FormatRuleParser(source)
parser = FormatRuleActionParser(source)
res = parser.parse()
self.error_sink = parser.error_sink
return FormatAstColor(color, res)
@@ -497,12 +504,139 @@ class FormatRuleParser(IterParser):
return FormatAstMulti(get_text_from_tokens(args[0]))
@dataclass
class EmitPythonCodeException(Exception):
error: object
class PythonCodeEmitter:
def __init__(self, context, text=None):
self.context = context
self.text = text or ""
self.var_counter = 0
self.variables = []
def add(self, text):
self.text += f" and {text}" if self.text else text
return self
def recognize(self, obj, as_name, root=True):
if isinstance(obj, str):
return self.recognize_str(obj, as_name)
elif isinstance(obj, (int, float)):
return self.recognize_int(obj, as_name)
elif isinstance(obj, Concept):
return self.recognize_concept(obj, as_name, root)
elif isinstance(obj, Expando):
return self.recognize_expando(obj, as_name, root)
else:
raise NotImplementedError()
def recognize_str(self, text, as_name):
if self.text:
self.text += " and "
if "'" in text and '"' in text:
self.text += f"{as_name} == '{text}'"
elif "'" in text:
self.text += f'{as_name} == "{text}"'
else:
self.text += f"{as_name} == '{text}'"
return self
def recognize_int(self, value, as_name):
if self.text:
self.text += " and "
self.text += f"{as_name} == {value}"
return self
def recognize_expando(self, value, as_name, root=True):
if self.text:
self.text += " and "
if not root:
as_name = self.add_variable(as_name)
self.text += f"isinstance({as_name}, Expando) and {as_name}.get_name() == '{value.get_name()}'"
return self
def recognize_concept(self, concept, as_name, root=True):
if self.text:
self.text += " and "
if not root:
as_name = self.add_variable(as_name)
if concept.get_hint(BuiltinConcepts.RECOGNIZED_BY) == RECOGNIZED_BY_NAME:
self.text += f"isinstance({as_name}, Concept) and {as_name}.name == '{concept.name}'"
elif concept.get_hint(BuiltinConcepts.RECOGNIZED_BY) == RECOGNIZED_BY_ID:
self.text += f"isinstance({as_name}, Concept) and {as_name}.id == '{concept.id}'"
else:
self.text += f"isinstance({as_name}, Concept) and {as_name}.key == '{concept.key}'"
if len(concept.get_metadata().variables) > 0:
# add variables constraints
evaluated = ensure_evaluated(self.context, concept, eval_body=False, metadata=["variables"])
if not self.context.sheerka.is_success(evaluated) and evaluated.key != concept.key:
raise EmitPythonCodeException(evaluated)
for k, v in concept.variables().items():
self.recognize(v, f"{as_name}.get_value('{k}')", root=False)
return self
def add_variable(self, target):
var_name = f"__x_{self.var_counter:02}__"
self.var_counter += 1
self.variables.append((var_name, target))
return var_name
def get_text(self):
if self.variables:
variables_as_str = '\n'.join([f"{k} = {v}" for k, v in self.variables])
return variables_as_str + "\n" + self.text
return self.text
class NoConditionFound(ErrorObj):
def __eq__(self, other):
return isinstance(other, NoConditionFound)
def __hash__(self):
return 0
@dataclass()
class RulePredicate:
source: str
evaluator: str
predicate: ReturnValueConcept
concept: Union[Concept, None]
class RuleCompiledPredicate:
"""
The 'when' expression is parsed to have a ReturnValueConcept or a Concept that can then be evaluated
Depending on the evaluator, the 'predicate' attribute or the 'concept' attribute will be used
"""
source: str # what was compiled # DO NOT REMOVE
action: str # sheerka action when the rule must be executed # can be removed
# when used as a list of predicate to iterate thru
evaluator: str # evaluator to use when the rule will be evaluated
predicate: ReturnValueConcept # compiled source as ReturnValue
concept: Union[Concept, None] # compiled source as concept
variables: Set[str] = None # TODO: set of required variables
@dataclass
class CompiledWhenResult:
"""
For a given source to compile (a given 'when')
List of RuleCompiledPredicate found
and list of Rete Conditions
The two ways of evaluating a 'when' are used by Sheerka
"""
compiled_predicates: List[RuleCompiledPredicate]
rete_disjunctions: List[AndConditions]
class SheerkaRuleManager(BaseService):
@@ -513,15 +647,18 @@ class SheerkaRuleManager(BaseService):
RULES_BY_NAME_ENTRY = "RuleManager:Rules_By_Name"
def __init__(self, sheerka):
super().__init__(sheerka)
super().__init__(sheerka, order=12)
self._format_rules = None # sorted by priority
self._exec_rules = None # sorted by priority
self.expression_parser = ExpressionParser()
def initialize(self):
self.sheerka.bind_service_method(self.create_new_rule, True, visible=False)
self.sheerka.bind_service_method(self.remove_rule, True)
self.sheerka.bind_service_method(self.get_rule_by_id, False)
self.sheerka.bind_service_method(self.get_rule_by_name, False)
self.sheerka.bind_service_method(self.dump_desc_rule, False, as_name="desc_rule")
self.sheerka.bind_service_method(self.get_format_rules, False, visible=False)
self.sheerka.bind_service_method(self.get_exec_rules, False, visible=False)
self.sheerka.bind_service_method(self.resolve_rule, False, visible=False)
cache = Cache().auto_configure(self.FORMAT_RULE_ENTRY)
@@ -531,6 +668,8 @@ class SheerkaRuleManager(BaseService):
cache = ListIfNeededCache().auto_configure(self.RULES_BY_NAME_ENTRY)
self.sheerka.om.register_cache(self.RULES_BY_NAME_ENTRY, cache, True, True)
self.sheerka.subscribe(EVENT_RULE_PRECEDENCE_MODIFIED, self.update_rules_priorities)
def initialize_deferred(self, context, is_first_time):
if is_first_time:
@@ -551,15 +690,17 @@ class SheerkaRuleManager(BaseService):
# compile all format the rules
for rule_id, rule_def in self.sheerka.om.get_all(self.FORMAT_RULE_ENTRY, cache_only=True).items():
rule = self.init_rule(context, rule_def)
self.init_rule(context, rule_def)
for rule_id, rule_def in self.sheerka.om.get_all(self.EXEC_RULE_ENTRY, cache_only=True).items():
self.init_rule(context, rule_def)
# update rules priorities
self.update_rules_priorities(context)
self.sheerka.subscribe(EVENT_RULE_PRECEDENCE_MODIFIED, self.update_rules_priorities)
def reset_state(self):
self._format_rules = None
self._exec_rules = None
def update_rules_priorities(self, context):
"""
@@ -575,50 +716,93 @@ class SheerkaRuleManager(BaseService):
rule.priority = rules_weights[rule.str_id]
self._format_rules = None
self._exec_rules = None
def init_rule(self, context, rule: Rule):
if rule.metadata.is_compiled:
return
return rule
if rule.compiled_predicate is None:
res = self.compile_when(context, self.NAME, rule.metadata.predicate)
if not isinstance(res, list):
rule.error_sink = [res.body]
return
rule.compiled_predicate = res
if rule.compiled_predicates is None:
try:
compiled_result = self.compile_when(context, self.NAME, rule.metadata.predicate)
rule.compiled_predicates = compiled_result.compiled_predicates
rule.rete_disjunctions = compiled_result.rete_disjunctions
except FailedToCompileError as ex:
rule.compiled_predicates = None
rule.rete_disjunctions = None
rule.error_sink = {"when": ex.cause}
if rule.compiled_action is None:
res = self.compile_print(context, rule.metadata.action)
if not res.status:
rule.error_sink = [res.body]
return
rule.compiled_action = res.body
compile_method = self.compile_print if rule.metadata.action_type == ACTION_TYPE_PRINT else self.compile_exec
# rule.variables = self.get_variables()
res = compile_method(context, rule.metadata.action)
if not res.status:
rule.compiled_action = None
if rule.error_sink is None:
rule.error_sink = {rule.metadata.action_type: res.body}
else:
rule.error_sink[rule.metadata.action_type] = res.body
else:
rule.compiled_action = res.body if rule.metadata.action_type == ACTION_TYPE_PRINT else res
rule.metadata.is_compiled = True
rule.metadata.is_enabled = True
rule.metadata.is_enabled = rule.error_sink is None
return rule
def compile_when(self, context, name, source):
# parser_input = self.sheerka.services[SheerkaExecute.NAME].get_parser_input(source)
parsed = parse_unrecognized(context,
source,
parsers="all",
who=name,
prop=Keywords.WHEN,
filter_func=only_successful)
def compile_when(self, context, who, source):
"""
Compile the predicate
:param context:
:param who: service which requested the compilation
:param source: what to compile
"""
if not parsed.status:
return parsed
# first, try to parse using expression parser
# -> Detect xxx and yyy or not zzz
if self.sheerka.isinstance(parsed.body, BuiltinConcepts.ONLY_SUCCESSFUL):
parsed = parsed.body.body
action = None
parsed = []
errors = []
all_rete_disjunctions = []
parsed_expr_ret = parse_expression(context, source)
if parsed_expr_ret.status:
conjunctions = parsed_expr_ret.body.body.parts if isinstance(parsed_expr_ret.body.body, AndNode) else \
[parsed_expr_ret.body.body]
return self.add_evaluators(source, parsed if hasattr(parsed, "__iter__") else [parsed])
# recognize __action == ''
if (action := self._recognized_action_definition(conjunctions[0].tokens)) is not None:
conjunctions = conjunctions[1:]
if len(conjunctions) == 0:
errors.append(NoConditionFound())
else:
# compile conditions
try:
return_values, rete_disjunctions = self.expression_parser.compile_conjunctions(context,
conjunctions,
who)
parsed.extend(return_values)
all_rete_disjunctions.extend(rete_disjunctions)
except FailedToCompileError as ex:
errors.append(ex.cause)
if len(parsed) == 0:
raise FailedToCompileError(errors)
try:
compiled_predicates = self.add_evaluators(context,
source,
action,
parsed if hasattr(parsed, "__iter__") else [parsed])
return CompiledWhenResult(compiled_predicates, all_rete_disjunctions)
except EmitPythonCodeException as ex:
raise FailedToCompileError([ex.error])
def compile_print(self, context, source):
parser = FormatRuleParser(source)
parser = FormatRuleActionParser(source)
parsed = parser.parse()
if parser.error_sink:
return self.sheerka.ret(self.NAME,
@@ -627,6 +811,16 @@ class SheerkaRuleManager(BaseService):
else:
return self.sheerka.ret(self.NAME, True, parsed)
def compile_exec(self, context, source):
parsed = parse_unrecognized(context,
source,
parsers="all",
who=self.NAME,
prop=Keywords.THEN,
filter_func=expect_one)
return parsed
def set_id_if_needed(self, rule: Rule):
"""
Set the id for the concept if needed
@@ -649,25 +843,52 @@ class SheerkaRuleManager(BaseService):
# set id before saving in db
self.set_id_if_needed(rule)
if rule.compiled_predicate and rule.compiled_action:
if rule.compiled_predicates and rule.compiled_action:
rule.metadata.is_compiled = True
rule.metadata.is_enabled = True
# save it
if rule.metadata.action_type == "print":
self.sheerka.om.put(self.FORMAT_RULE_ENTRY, rule.metadata.id, rule)
if rule.metadata.action_type == ACTION_TYPE_PRINT:
sheerka.om.put(self.FORMAT_RULE_ENTRY, rule.metadata.id, rule)
self._format_rules = None
else:
self.sheerka.om.put(self.EXEC_RULE_ENTRY, rule.metadata.id, rule)
sheerka.om.put(self.EXEC_RULE_ENTRY, rule.metadata.id, rule)
self._exec_rules = None
# save by name if needed
if rule.metadata.name:
self.sheerka.om.put(self.RULES_BY_NAME_ENTRY, rule.metadata.name, rule)
# rule is created. publish the event
sheerka.publish(context, EVENT_RULE_CREATED, rule)
# process the return if needed
ret = sheerka.ret(self.NAME, True, sheerka.new(BuiltinConcepts.NEW_RULE, body=rule))
return ret
def remove_rule(self, context, rule):
"""
Remove a rule
"""
rule = self.resolve_rule(context, rule)
if rule is None:
return
# rule will be deleted. publish the event first, as the rule may not be available after
self.sheerka.publish(context, EVENT_RULE_DELETED, rule)
if rule.metadata.action_type == ACTION_TYPE_PRINT:
self.sheerka.om.delete(self.FORMAT_RULE_ENTRY, rule.metadata.id)
self._format_rules = None
else:
self.sheerka.om.delete(self.EXEC_RULE_ENTRY, rule.metadata.id)
self._exec_rules = None
if rule.metadata.name:
self.sheerka.om.delete(self.RULES_BY_NAME_ENTRY, rule.metadata.name)
return self.sheerka.ret(self.NAME, True, self.sheerka.new(BuiltinConcepts.SUCCESS))
def init_builtin_rules(self, context):
# self.sheerka.init_log.debug("Initializing default rules")
rules = [
@@ -760,29 +981,6 @@ class SheerkaRuleManager(BaseService):
return rule
def dump_desc_rule(self, rules):
"""
dumps the definition of a rule
:param rules:
:return:
"""
ensure_rule(rules)
if not hasattr(rules, "__iter__"):
rules = [rules]
first = True
for rule in rules:
if not first:
self.sheerka.log.info("")
self.sheerka.log.info(f"id : {rule.id}")
self.sheerka.log.info(f"name : {rule.metadata.name}")
self.sheerka.log.info(f"type : {rule.metadata.action_type}")
self.sheerka.log.info(f"predicate : {rule.metadata.predicate}")
self.sheerka.log.info(f"action : {rule.metadata.action}")
self.sheerka.log.info(f"compiled : {rule.metadata.is_compiled}")
self.sheerka.log.info(f"enabled : {rule.metadata.is_enabled}")
def get_format_rules(self):
if self._format_rules:
return self._format_rules
@@ -792,32 +990,56 @@ class SheerkaRuleManager(BaseService):
reverse=True)
return self._format_rules
def add_evaluators(self, source, ret_vals):
def get_exec_rules(self):
if self._exec_rules:
return self._exec_rules
self._exec_rules = sorted(self.sheerka.om.list(self.EXEC_RULE_ENTRY, cache_only=True),
key=operator.attrgetter('priority'),
reverse=True)
return self._exec_rules
def add_evaluators(self, context, source, action, ret_vals):
"""
Browse the ReturnValueConcepts to determine the evaluator to use
Returns a list of tuple (evaluator_name, return_value)
Returns a list of RulePredicate, basically a tuple (evaluator_name, return_value)
:param context:
:param source:
:param ret_vals:
:return:
"""
def get_rule_predicate_from_concept(c):
if is_a_question(context, c):
return RuleCompiledPredicate(source, action, ConceptEvaluator.NAME, r, c)
else:
to_parse = PythonCodeEmitter(context, "__ret.status").recognize_concept(c, "__ret.body").get_text()
return RuleCompiledPredicate(source, action, PythonEvaluator.NAME, parse_python(context, to_parse),
None)
res = []
for r in ret_vals:
underlying = self.sheerka.objvalue(r)
if isinstance(underlying, PythonNode):
res.append(RulePredicate(source, PythonEvaluator.NAME, r, None))
res.append(RuleCompiledPredicate(source, action, PythonEvaluator.NAME, r, None))
elif isinstance(underlying, SourceCodeWithConceptNode):
res.append(RulePredicate(source, PythonEvaluator.NAME, r, None))
res.append(RuleCompiledPredicate(source, action, PythonEvaluator.NAME, r, None))
elif isinstance(underlying, SourceCodeNode):
res.append(RulePredicate(source, PythonEvaluator.NAME, r, None))
res.append(RuleCompiledPredicate(source, action, PythonEvaluator.NAME, r, None))
elif isinstance(underlying, Concept):
res.append(RulePredicate(source, ConceptEvaluator.NAME, r, underlying))
res.append(get_rule_predicate_from_concept(underlying))
elif hasattr(underlying, "__iter__") and len(underlying) == 1 and isinstance(underlying[0], ConceptNode):
res.append(RulePredicate(source, ConceptEvaluator.NAME, r, underlying[0].concept))
res.append(get_rule_predicate_from_concept(underlying[0].concept))
else:
raise NotImplementedError(r)
return res
def resolve_rule(self, context, obj):
"""
Given obj, try to find the corresponding rule
:param context:
:param obj:
"""
if obj is None:
return None
@@ -841,7 +1063,7 @@ class SheerkaRuleManager(BaseService):
(rule := self._inner_get_by_id(str(rule_id))) is not None:
return rule
else:
return obj
return self._inner_get_by_id(obj.id)
return None
@@ -855,3 +1077,41 @@ class SheerkaRuleManager(BaseService):
return rule
return None
@staticmethod
def _recognized_action_definition(tokens):
"""
Tries to recognize the pattern __action = xxx in the tokens
"""
iter_token = iter(tokens)
try:
token = next(iter_token)
if token.value != "__action":
return None
token = next(iter_token)
if token.type == TokenKind.WHITESPACE:
token = next(iter_token)
if token.type != TokenKind.EQUALSEQUALS:
return None
token = next(iter_token)
if token.type == TokenKind.WHITESPACE:
token = next(iter_token)
if token.type == TokenKind.STRING:
return token.strip_quote
except StopIteration:
pass
return None
@staticmethod
def _get_parsed_concept(context, return_value):
if not context.sheerka.isinstance(return_value.body, BuiltinConcepts.PARSER_RESULT):
return None
if isinstance(return_value.body.body, Concept):
return return_value.body.body
if isinstance(return_value.body.body, ConceptNode):
return return_value.body.body.concept
return None
@@ -41,9 +41,9 @@ class SheerkaVariableManager(BaseService):
INTERNAL_VARIABLES_ENTRY = "VariableManager:InternalVariables" # internal to current process (can store lambda)
def __init__(self, sheerka):
super().__init__(sheerka)
super().__init__(sheerka, order=3)
self.bound_variables = {
self.sheerka.name: {"enable_process_return_values", "save_execution_context"}
self.sheerka.name: {"enable_process_return_values", "save_execution_context", "enable_process_rules"}
}
def initialize(self):
+8 -2
View File
@@ -1,6 +1,6 @@
from dataclasses import dataclass
from core.global_symbols import NotFound
from core.global_symbols import NotFound, ErrorObj
from core.utils import sheerka_deepcopy
@@ -14,8 +14,9 @@ class BaseService:
Base class for services
"""
def __init__(self, sheerka):
def __init__(self, sheerka, order=999):
self.sheerka = sheerka
self.order = order # initialisation order. The lowest is initialized first
def initialize(self):
"""
@@ -46,3 +47,8 @@ class BaseService:
Store/record the value of an attribute
"""
self.sheerka.record_var(context, self.NAME, var_name, getattr(self, var_name))
@dataclass()
class FailedToCompileError(Exception, ErrorObj):
cause: list