Fixed #12
Fixed #13
Fixed #14
This commit is contained in:
2023-05-08 17:50:28 +02:00
parent 21a397861a
commit e41094f908
95 changed files with 12168 additions and 260 deletions
+7
View File
@@ -0,0 +1,7 @@
class BuiltinConcepts:
SHEERKA = "__SHEERKA"
NEW_CONCEPT = "__NEW_CONCEPT"
UNKNOWN_CONCEPT = "__UNKNOWN_CONCEPT"
USER_INPUT = "__USER_INPUT"
PARSER_INPUT = "__PARSER_INPUT"
+35
View File
@@ -0,0 +1,35 @@
from common.utils import compute_hash
from core.ExecutionContext import ExecutionContext
class SheerkaException(Exception):
pass
class ErrorContext:
"""
This class represents the result of a data flow processing
"""
def __init__(self, who: str, context: ExecutionContext, value: object = None):
self.who = who
self.context = context
self.value = value
self.parents = None
def __repr__(self):
return f"Error(who={self.who}, context_id={self.context.long_id}, value={self.value})"
def __eq__(self, other):
if id(self) == id(other):
return True
if not isinstance(other, ErrorContext):
return False
return self.who == other.who and \
self.context.id == other.context.id and \
self.value == other.value
def __hash__(self):
return hash((self.who, self.context.id, compute_hash(self.value)))
+69
View File
@@ -0,0 +1,69 @@
import hashlib
from datetime import datetime
class Event(object):
"""
Class that represents something that modifies the state of the system
"""
def __init__(self, message="", user_id="", date=None, parents=None):
self.user_id: str = user_id # id of the user that triggers the modification
self.date: datetime | None = date or datetime.now() # when
self.message: str = message # user input or whatever that modifies the system
self.parents: list[str] = parents # digest(s) of the parent(s) of this event
self._digest: str | None = None # digest of the event
def __str__(self):
return f"{self.date.strftime('%d/%m/%Y %H:%M:%S')} {self.message}"
def __repr__(self):
return f"{self.get_digest()[:12]} {self.message}"
def get_digest(self):
"""
Returns the digest of the event
:return: sha256 of the event
"""
if self._digest:
return self._digest
if self.user_id == "":
# only possible during the unit test
# We use this little trick to speed up the unit test
self._digest = self.message[6:] if self.message.startswith("TEST::") else "xxx"
return self._digest
if not isinstance(self.message, str):
raise NotImplementedError(f"message={self.message}")
to_hash = f"Event:{self.user_id}{self.date}{self.message}{self.parents}".encode("utf-8")
self._digest = hashlib.sha256(to_hash).hexdigest()
return self._digest
def to_dict(self):
return self.__dict__
def from_dict(self, as_dict):
self.user_id = as_dict["user_id"]
self.date = datetime.fromisoformat(as_dict["date"])
self.message = as_dict["message"]
self.parents = as_dict["parents"]
self._digest = as_dict["_digest"] # freeze the digest
def __eq__(self, other):
if id(self) == id(other):
return True
if isinstance(other, Event):
return (self.user_id == other.user_id and
self.date == other.date and
self.message == other.message and
self.parents == other.parents)
return False
def __hash__(self):
return hash(self.get_digest())
+180 -1
View File
@@ -1,2 +1,181 @@
from __future__ import annotations
import time
from core.Event import Event
class ExecutionContextActions:
TESTING = "Testing"
INIT_SHEERKA = "Init Sheerka"
EVALUATE_USER_INPUT = "Evaluate user input"
EVALUATING_STEP = "Evaluating step"
EVALUATING_ITERATION = "Evaluating iteration"
BEFORE_PARSING = "Before parsing"
PARSING = "Parsing"
AFTER_PARSING = "After parsing"
BEFORE_EVALUATION = "Before evaluation"
EVALUATION = "Evaluation"
AFTER_EVALUATION = "After Evaluation"
class ContextHint:
REDUCE_CONCEPTS = "Reduce Concepts" # to tell the process to only keep the meaningful results
ids = {} # keep track of the next execution context id, for a given event id
def get_next_id(event_digest):
"""
For a given event, give the next id
:param event_digest:
:type event_digest:
:return:
:rtype:
"""
if event_digest in ids:
ids[event_digest] += 1
else:
ids[event_digest] = 0
return ids[event_digest]
class ExecutionContext:
pass
"""
To keep track of the execution of a request
Note that the protected hints are working correctly only if the hint is added BEFORE the creation of the child
"""
def __init__(self,
who: str,
event: Event,
sheerka,
action: ExecutionContextActions,
action_context: object,
desc: str = None,
logger=None,
global_hints=None,
protected_hints=None,
parent: ExecutionContext = None):
self._id = get_next_id(event.get_digest())
self._parent = parent
self._children = []
self._start = 0 # when the execution starts (to measure elapsed time)
self._stop = 0 # when the execution stops (to measure elapses time)
self._logger = logger
self.who = who # who is asking
self.event = event # what was the (original) trigger
self.sheerka = sheerka # sheerka
self.action = action
self.action_context = action_context
self.desc = desc # human description of what is going on
self.private_hints = set()
self.protected_hints = set() if protected_hints is None else protected_hints.copy()
self.global_hints = set() if global_hints is None else global_hints
self.inputs = {} # what were the parameters of the execution context
self.values = {} # what was produced by the execution context
def __repr__(self):
msg = f"ExecutionContext(who={self.who}, id={self._id}, action={self.action}, context={self.action_context}"
if self.desc:
msg += f", desc='{self.desc}'"
msg += ")"
return msg
def __eq__(self, other):
if id(self) == id(other):
return True
if not isinstance(other, ExecutionContext):
return False
return self.long_id == other.long_id
def __hash__(self):
return hash(self.long_id)
@property
def long_id(self):
return f"{self.event.get_digest()}:{self._id}"
@property
def id(self):
return self._id
@property
def elapsed(self):
if self._start == 0:
return 0
return (self._stop if self._stop > 0 else time.time_ns()) - self._start
@property
def elapsed_str(self):
nano_sec = self.elapsed
dt = nano_sec / 1e6
return f"{dt} ms" if dt < 1000 else f"{dt / 1000} s"
def add_inputs(self, **kwargs):
"""
When entering stacking an ExecutionContext, list of variable that are worth to trace
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
self.inputs.update(kwargs)
return self
def add_values(self, **kwargs):
"""
When popping from an ExecutionContext, list of variable that are worth to trace
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
self.values.update(kwargs)
return self
def push(self,
who: str,
action: ExecutionContextActions,
action_context: object,
desc: str = None,
logger=None):
child = ExecutionContext(
who,
self.event,
self.sheerka,
action,
action_context,
desc,
logger or self._logger,
self.global_hints,
self.protected_hints,
self
)
self._children.append(child)
return child
def get_children(self, level=-1):
"""
recursively look for children
:return:
:rtype:
"""
for child in self._children:
yield child
if level != 1:
yield from child.get_children(level - 1)
def __enter__(self):
self._start = time.time_ns()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._stop = time.time_ns()
+32
View File
@@ -0,0 +1,32 @@
from typing import Any
from common.utils import compute_hash
class ReturnValue:
"""
This class represents the result of a data flow processing
"""
def __init__(self, who: str = None, status: bool = None, value: Any = None, parents: list = None):
self.who = who
self.status = status
self.value = value
self.parents = parents
def __repr__(self):
return f"ReturnValue(who={self.who}, status={self.status}, value={self.value})"
def __eq__(self, other):
if id(self) == id(other):
return True
if not isinstance(other, ReturnValue):
return False
return self.who == other.who and \
self.status == other.status and \
self.value == other.value
def __hash__(self):
return hash((self.who, self.status, compute_hash(self.value)))
+323 -1
View File
@@ -1,2 +1,324 @@
import inspect
import logging
import sys
from dataclasses import dataclass
from operator import attrgetter
from os import path
from typing import Callable
from caching.Cache import Cache
from caching.IncCache import IncCache
from common.utils import get_logger_name, get_sub_classes, import_module_and_sub_module
from core.BuiltinConcepts import BuiltinConcepts
from core.ErrorContext import ErrorContext
from core.Event import Event
from core.ExecutionContext import ContextHint, ExecutionContext, ExecutionContextActions
from core.ReturnValue import ReturnValue
from core.concept import Concept, ConceptMetadata
from ontologies.SheerkaOntologyManager import SheerkaOntologyManager
from server.authentication import User
EXECUTE_STEPS = [
ExecutionContextActions.BEFORE_PARSING,
ExecutionContextActions.PARSING,
ExecutionContextActions.AFTER_PARSING,
ExecutionContextActions.BEFORE_EVALUATION,
ExecutionContextActions.EVALUATION,
ExecutionContextActions.AFTER_EVALUATION
]
@dataclass
class SheerkaConfig:
"""
After each execution, persist the whole executions flow as a file
This file will be used by the debugger
"""
save_execution_context: bool = True
@dataclass
class SheerkaMethod:
"""
Wrapper to sheerka method, to indicate if it's safe to call
"""
name: str
service: str
method: Callable
has_side_effect: bool
def __repr__(self):
return self.name
def __hash__(self):
return hash((self.name, self.service))
class Sheerka:
pass
OBJECTS_IDS_ENTRY = "Objects_Ids"
CHICKEN_AND_EGG_CONCEPTS_ENTRY = "Chicken_And_Egg_Concepts"
def __init__(self):
"""
Engine of the so called Sheerka
"""
self.name = "Sheerka"
self.om: SheerkaOntologyManager = None
self.config = SheerkaConfig()
self.during_initialisation = False
self.log = logging.getLogger(get_logger_name(__name__))
self.init_log = logging.getLogger(get_logger_name("init." + __name__))
self.services = {} # sheerka plugins
self.evaluators = {} # cache for evaluators
self.sheerka_methods = {}
self.methods_with_context = set() # only the names, the method is defined in sheerka_methods
self.global_context_hints = set()
def bind_service_method(self, service_name, bound_method, can_modify_state, as_name=None, visible=True):
"""
Bind service method to sheerka instance for ease of use ?
:param service_name:
:param bound_method:
:param can_modify_state: Can update the state of Sheerka => can produce side_effect
:param as_name: give another name to the method
:param visible: make the method visible to Sheerka
:return:
"""
if as_name is None:
as_name = bound_method.__name__
if visible:
signature = inspect.signature(bound_method)
if len(signature.parameters) > 0 and list(signature.parameters.keys())[0] == "context":
self.methods_with_context.add(as_name)
self.sheerka_methods[as_name] = SheerkaMethod(as_name, service_name, bound_method, can_modify_state)
setattr(self, bound_method.__name__, bound_method)
def initialize(self, root_folder: str = None, **kwargs):
"""
Starting Sheerka
Loads the current configuration
Notes that when it's the first time, it also creates the needed working folders
:param root_folder: root configuration folder
:return: ReturnValue(Success or Error)
"""
if root_folder is None:
root_folder = path.abspath(path.join(path.expanduser("~"), ".sheerka"))
self.initialize_logging(False, root_folder)
self.config.save_execution_context = kwargs.get("save_execution_context", self.config.save_execution_context)
try:
self.init_log.info("Starting Sheerka")
self.during_initialisation = True
# from sheerkapickle.sheerka_handlers import initialize_pickle_handlers
# initialize_pickle_handlers()
self.om = SheerkaOntologyManager(self, root_folder)
# self.builtin_cache, self.builtin_cache_by_class_name = self.get_builtins_classes_as_dict()
self.initialize_caching()
self.initialize_evaluators()
self.initialize_services()
# self.initialize_builtin_evaluators()
# self.om.init_subscriptions()
event = Event("Initializing Sheerka.", user_id=self.name)
self.om.save_event(event)
with ExecutionContext(self.name,
event,
self,
ExecutionContextActions.INIT_SHEERKA,
None,
desc="Initializing Sheerka.") as exec_context:
if self.om.current_sdp().first_time:
self.first_time_initialisation(exec_context)
self.initialize_services_deferred(exec_context, self.om.current_sdp().first_time)
res = ReturnValue(self.name, True, self.get_startup_config())
exec_context.add_values(return_values=res)
if self.om.is_dirty():
self.om.commit(exec_context)
if self.config.save_execution_context:
self.om.save_execution_context(exec_context, is_admin=True)
# append the other ontologies if needed
self.om.freeze()
self.initialize_ontologies(exec_context)
# self.init_log.debug(f"Sheerka successfully initialized")
except IOError as e:
res = ReturnValue(self.name, False, ErrorContext(self.name, exec_context, e))
finally:
self.during_initialisation = False
return res
@staticmethod
def initialize_logging(is_debug, root_folder):
if is_debug:
# log_format = "%(asctime)s %(name)s"
log_format = "[%(levelname)s] [%(name)s]"
log_format += " %(message)s"
log_level = logging.DEBUG
else:
log_format = "%(message)s"
log_level = logging.INFO
logging.basicConfig(format=log_format, level=log_level, handlers=[logging.StreamHandler(sys.stdout)])
logging.addLevelName(logging.ERROR, f"\033[1;41m%s\033[1;0m{logging.getLevelName(logging.ERROR)}")
def initialize_ontologies(self, context):
ontologies = self.om.current_sdp().load_ontologies()
if not ontologies:
return
for ontology_name in list(reversed(ontologies))[1:]:
self.om.push_ontology(ontology_name, False)
# self.initialize_services_deferred(context, False)
def first_time_initialisation(self, context):
pass
# self.record_var(context, self.name, "save_execution_context", self.save_execution_context)
def initialize_caching(self):
cache = IncCache().auto_configure(self.OBJECTS_IDS_ENTRY)
self.om.register_cache(self.OBJECTS_IDS_ENTRY, cache)
cache = Cache().auto_configure(self.CHICKEN_AND_EGG_CONCEPTS_ENTRY)
self.om.register_cache(self.CHICKEN_AND_EGG_CONCEPTS_ENTRY, cache, persist=False)
def initialize_services(self):
"""
Introspect to find services and bind them
:return:
"""
self.init_log.info("Initializing services")
import_module_and_sub_module('core.services')
base_class = "core.services.BaseService.BaseService"
services = [service(self) for service in get_sub_classes("core.services", base_class)]
services.sort(key=attrgetter("order"))
for service in services:
if hasattr(service, "initialize"):
service.initialize()
self.services[service.NAME] = service
self.init_log.info(f"{len(services)} service(s) found.")
def initialize_services_deferred(self, context, is_first_time):
"""
Initialize part of services that may take some time or that need the execution context
:return:
"""
self.init_log.debug(f"Initializing services (deferred, {is_first_time=})")
for service in self.services.values():
if hasattr(service, "initialize_deferred"):
service.initialize_deferred(context, is_first_time)
def initialize_evaluators(self):
self.init_log.info("Initializing evaluators")
base_class1 = "evaluators.base_evaluator.OneReturnValueEvaluator"
base_class2 = "evaluators.base_evaluator.AllReturnValuesEvaluator"
import_module_and_sub_module('evaluators')
evaluators = [evaluator() for evaluator in get_sub_classes("evaluators", base_class1)] + \
[evaluator() for evaluator in get_sub_classes("evaluators", base_class2)]
self.evaluators = {e.NAME: e for e in evaluators}
self.init_log.info(f"{len(evaluators)} evaluator(s) found.")
def bind_services_methods(self):
# init methods
# self.bind_service_method(self.name, self.test, False)
# self.bind_service_method(self.name, self.test_using_context, False)
# self.bind_service_method(self.name, self.test_dict, False)
# self.bind_service_method(self.name, self.test_error, False)
# self.bind_service_method(self.name, self.is_sheerka, False)
# self.bind_service_method(self.name, self.objvalue, False)
pass
def get_startup_config(self):
"""
Return a dictionary with current configuration, used for initialization
:return:
:rtype:
"""
return {
"config": self.config.__dict__
}
def publish(self, context, topic, data=None):
"""
To be removed as it must be part of the EventManager service
:param context:
:type context:
:param topic:
:type topic:
:param data:
:type data:
:return:
:rtype:
"""
pass
def evaluate_user_input(self, command: str, user: User):
self.log.info("Processing '%s' from '%s'", command, user.email)
event = Event(command, user_id=user.email)
self.om.save_event(event)
with ExecutionContext(user.email,
event,
self,
ExecutionContextActions.EVALUATE_USER_INPUT,
command,
desc=f"Evaluating '{command}'",
global_hints=self.global_context_hints.copy()) as exec_context:
user_input = ReturnValue(self.name, True, self.newn(BuiltinConcepts.USER_INPUT, command=command))
exec_context.private_hints.add(ContextHint.REDUCE_CONCEPTS)
# KSI : 2023-04-30
# Il me manque le execute et toute la classe SheerkaProcessUserInput
exec_context.add_inputs(user_input=user_input)
ret = self.execute(exec_context, [user_input], EXECUTE_STEPS)
exec_context.add_values(return_values=ret)
if self.om.is_dirty():
self.om.commit(exec_context)
return ret
def isinstance(self, a, b):
"""
Returns true if 'a' is a concept of type 'b'
Note that this function can be moved into ConceptManager
I keep it here for quick access
:param a:
:type a:
:param b:
:type b:
:return:
:rtype:
"""
if not isinstance(a, Concept):
return False
if isinstance(b, (Concept, ConceptMetadata)):
return a.id == b.id
if b.startswith("c:#"):
return a.id == b[3:-1]
return a.key == b
View File
+201
View File
@@ -0,0 +1,201 @@
from dataclasses import dataclass
from common.global_symbols import NotFound, NotInit
class ConceptDefaultProps:
"""
Lists metadata that can contains some code
"""
WHERE = "#where#"
PRE = "#pre#"
POST = "#post#"
BODY = "#body#"
RET = "#ret#"
DefaultProps = [v for k, v in ConceptDefaultProps.__dict__.items() if not k.startswith("_")]
class DefinitionType:
DEFAULT = "Default"
BNF = "Bnf"
@dataclass
class ConceptMetadata:
"""
Static information of the Concept
"""
id: str # unique identifier for a concept. The id will never be modified (but the key can)
name: str
key: str
is_builtin: bool
is_unique: bool
body: str # main method, can also be the value of the concept
where: str # condition to recognize variables in name
pre: str # list of preconditions before calling the main function
post: str # list of post conditions after calling the main function
ret: str # variable to return when a concept is recognized
definition: str # regex used to define the concept
definition_type: DefinitionType # definition can be done with something else than regex
desc: str # possible description for the concept
autouse: bool # indicates if eval must be automatically called on the concept once validated
bound_body: str # which property must be considered have default value for the concept
props: dict # hashmap of properties, values
variables: tuple # list of concept variables(tuple), with their default values
parameters: tuple # list of variables that are part of the name of the concept
digest: str = None
all_attrs: tuple = None
@dataclass
class ConceptRuntimeInfo:
"""
Dynamic information of the Concept
They are related to the instance of the concept
"""
is_evaluated: bool = False # True is the concept is evaluated by sheerka.eval_concept()
need_validation: bool = False # True if the properties of the concept need to be validated
recognized_by: str = None # RECOGNIZED_BY_ID, RECOGNIZED_BY_NAME, RECOGNIZED_BY_KEY (from Sheerka.py)
def copy(self):
return ConceptRuntimeInfo(self.is_evaluated,
self.need_validation,
self.recognized_by)
class Concept:
"""
Default concept object
A concept is the base object of our universe
Everything is a concept
"""
def __init__(self, metadata: ConceptMetadata):
self._metadata: ConceptMetadata = metadata
self._compiled = {} # cached ast for the where, pre, post and body parts and variables
self._compiled_context_hints = {} # context hints to use when evaluating compiled
self._bnf = None # compiled bnf expression
self._runtime_info = ConceptRuntimeInfo() # runtime settings for the concept
self._all_attrs = None
def __repr__(self):
text = f"({self._metadata.id}){self._metadata.name}"
if self._metadata.pre:
text += f", #pre={self._metadata.pre}"
for attr in [attr for attr in self.all_attrs() if not attr.startswith("#")]:
text += f", {attr}={self.get_value(attr)}"
return text
def __eq__(self, other):
# I don't want this test to be part of the recursion
# So let's just get rif ogf it
if not isinstance(other, Concept):
return False
# I chose to use an iterative algorithm in order to be able to spot circular reference
# without inner functions.
# I also think that it's a better approach for a function that can be massively called
stack = [self, other]
id_self = id(self)
while stack:
right = stack.pop()
left = stack.pop()
if id(left) == id(right):
return True
# 1. in order for two concepts to be equal, they must have the same definition
# 2. They must have the same properties and variables
if left.get_definition_digest() != right.get_definition_digest():
return False
if left.all_attrs() != right.all_attrs():
return False
for attr in left.all_attrs():
value = left.get_value(attr)
other_value = right.get_value(attr)
if isinstance(value, Concept) and isinstance(other_value, Concept):
if id(value) == id_self or id(other_value) == id_self:
# infinite recursion detected
pass
else:
stack.extend([value, other_value])
else:
if value != other_value:
return False
return True
def __hash__(self):
return self._metadata.digest
@property
def id(self):
return self._metadata.id
@property
def name(self):
return self._metadata.name
@property
def key(self):
return self._metadata.key
@property
def body(self):
return self.get_value(ConceptDefaultProps.BODY)
@property
def str_id(self):
return f"c:#{self.id}:" if self.id else f"c:{self.name}:"
def get_definition_digest(self):
return self._metadata.digest
def all_attrs(self):
if self._all_attrs is None:
return self._metadata.all_attrs
return self._all_attrs
def get_metadata(self) -> ConceptMetadata:
return self._metadata
def set_value(self, name: str, value: object):
"""
Set the resolved value of a metadata or a variable (not the metadata itself)
:param name:
:param value:
:return:
"""
setattr(self, name, value)
if name == self._metadata.bound_body:
setattr(self, ConceptDefaultProps.BODY, value)
elif self._metadata.bound_body and name == ConceptDefaultProps.BODY:
setattr(self, self._metadata.bound_body, value)
return self
def get_value(self, name: str):
"""
Gets the resolved value of a metadata
:param name:
:return:
"""
try:
return getattr(self, name)
except AttributeError:
return NotInit if name in self.all_attrs() else NotFound
def get_runtime_info(self):
return self._runtime_info
-31
View File
@@ -1,31 +0,0 @@
class CustomType:
"""
Base class for custom types used in Sheerka
A custom type is a type that has only one instance across the application and have a semantic meaning
For example the type 'None' is a singleton which have a semantic meaning.
We need to define others in Sheerka
"""
def __init__(self, value):
self.value = value
def __repr__(self):
return self.value
def __eq__(self, other):
return isinstance(other, CustomType) and self.value == other.value
def __hash__(self):
return hash(self.value)
class NotFoundType(CustomType):
"""
Using when an entry in not found in Cache or in sdp
"""
def __init__(self):
super(NotFoundType, self).__init__("**NotFound**")
NotFound = NotFoundType()
+49
View File
@@ -0,0 +1,49 @@
from common.global_symbols import NotFound
from common.utils import sheerka_deepcopy
from core.Sheerka import Sheerka
class BaseService:
"""
Base class for services
"""
def __init__(self, sheerka: Sheerka, order=999):
self.sheerka = sheerka
self.order = order # initialisation order. The lowest is initialized first
def initialize(self):
"""
Adds cache or bind methods
:return:
"""
pass
def state_properties(self):
pass
def push_state(self, context):
"""
Use variable Manager to store the state of the service
"""
args = self.state_properties()
if args:
for prop_name in args:
self.sheerka.record_var(context, self.NAME, prop_name, sheerka_deepcopy(getattr(self, prop_name)))
def pop_state(self):
"""
Use Variable Manager to restore the state of a service
:return:
"""
args = self.state_properties()
if args:
for prop_name in args:
if (value := self.sheerka.load_var(self.NAME, prop_name)) is not NotFound:
setattr(self, prop_name, value)
def store_var(self, context, var_name):
"""
Store/record the value of an attribute
"""
self.sheerka.record_var(context, self.NAME, var_name, getattr(self, var_name))
+343
View File
@@ -0,0 +1,343 @@
import hashlib
import logging
from dataclasses import dataclass
from caching.Cache import Cache
from caching.FastCache import FastCache
from caching.ListIfNeededCache import ListIfNeededCache
from common.global_symbols import NotFound, NotInit, VARIABLE_PREFIX
from common.utils import get_logger_name
from core.BuiltinConcepts import BuiltinConcepts
from core.ErrorContext import ErrorContext, SheerkaException
from core.ExecutionContext import ExecutionContext
from core.ReturnValue import ReturnValue
from core.concept import Concept, ConceptMetadata, DefaultProps, DefinitionType
from core.services.BaseService import BaseService
from parsers.tokenizer import TokenKind, Tokenizer, strip_tokens
PROPERTIES_FOR_DIGEST = ("name", "key",
"definition", "definition_type",
"is_builtin", "is_unique",
"where", "pre", "post", "body", "ret",
"desc", "bound_body", "autouse", "props", "variables", "parameters")
@dataclass
class ConceptAlreadyDefined(SheerkaException):
concept: ConceptMetadata
already_defined_id: str
@dataclass
class InvalidBnf(SheerkaException):
bnf: str
@dataclass
class FirstItemError(SheerkaException):
pass
class ConceptManager(BaseService):
"""
The service is used for the administration of concepts
You can define new concept, modify or delete them
There are also function to help retrieve them easily (like first token cache)
Already instantiated concepts are managed by the Memory service
"""
NAME = "ConceptManager"
USER_CONCEPTS_IDS = "User_Concepts_IDs" # incremented everytime a new concept is created
CONCEPTS_BY_ID_ENTRY = "ConceptManager:Concepts_By_ID" # to store all the concepts
CONCEPTS_BY_KEY_ENTRY = "ConceptManager:Concepts_By_Key"
CONCEPTS_BY_NAME_ENTRY = "ConceptManager:Concepts_By_Name"
CONCEPTS_BY_HASH_ENTRY = "ConceptManager:Concepts_By_Hash" # sto
def __init__(self, sheerka):
super().__init__(sheerka, order=11)
self.log = logging.getLogger(get_logger_name(__name__))
self.init_log = logging.getLogger(get_logger_name("init." + __name__))
self.bnf_expr_cache = FastCache()
def initialize(self):
self.init_log.debug(f"Initializing ConceptManager, order={self.order}")
self.sheerka.bind_service_method(self.NAME, self.define_new_concept, True)
self.sheerka.bind_service_method(self.NAME, self.newn, True)
self.sheerka.bind_service_method(self.NAME, self.newi, True)
register_concept_cache = self.sheerka.om.register_concept_cache
# Cache of concept metadata, organized by id
cache = Cache().auto_configure(self.CONCEPTS_BY_ID_ENTRY)
register_concept_cache(self.CONCEPTS_BY_ID_ENTRY, cache, lambda c: c.id, True)
cache = ListIfNeededCache().auto_configure(self.CONCEPTS_BY_KEY_ENTRY)
register_concept_cache(self.CONCEPTS_BY_KEY_ENTRY, cache, lambda c: c.key, True)
cache = ListIfNeededCache().auto_configure(self.CONCEPTS_BY_NAME_ENTRY)
register_concept_cache(self.CONCEPTS_BY_NAME_ENTRY, cache, lambda c: c.name, True)
cache = ListIfNeededCache().auto_configure(self.CONCEPTS_BY_HASH_ENTRY)
register_concept_cache(self.CONCEPTS_BY_HASH_ENTRY, cache, lambda c: c.digest, True)
def initialize_deferred(self, context, is_first_time):
if is_first_time:
self.sheerka.om.put(self.sheerka.OBJECTS_IDS_ENTRY, self.USER_CONCEPTS_IDS, 1000)
_ = self._create_builtin_concept
_(1, BuiltinConcepts.SHEERKA, desc="Sheerka")
_(2, BuiltinConcepts.NEW_CONCEPT, desc="On new concept creation", variables=("metadata",))
_(3, BuiltinConcepts.UNKNOWN_CONCEPT, desc="Unknown concept", variables=("requested_name", "requested_id"))
_(4, BuiltinConcepts.USER_INPUT, desc="Any external input", variables=("command",))
_(5, BuiltinConcepts.PARSER_INPUT, desc="tokenized input", variables=("pi",))
self.init_log.debug('%s builtin concepts created',
len(self.sheerka.om.current_cache_manager().concept_caches))
def define_new_concept(self, context: ExecutionContext,
name: str,
is_builtin: bool = False, # is the concept defined Sheerka
is_unique: bool = False, # is the concept a singleton
body: str = "", # return value of the concept
where: str = "", # condition to recognize variables in name
pre: str = "", # list of preconditions before calling the main function
post: str = "", # list of post conditions after calling the main function
ret: str = "", # variable to return when a concept is recognized
definition: str = "", # regex used to define the concept
definition_type: DefinitionType = DefinitionType.DEFAULT,
autouse: bool = False, # indicate if the concept must be automatically evaluated
bound_body: str = None, #
desc: str = "", # possible description for the concept
props: dict = None, # hashmap of default properties
variables: list = None, # list of concept variables(tuple), with their default values
parameters: list = None # list of variables that are part of the name of the concept
) -> ReturnValue:
"""
Adds the definition of a new concept
:return:
:rtype:
"""
concept_key = self.create_concept_key(name, definition, variables)
concept_id = "waiting for id"
metadata = ConceptMetadata(
concept_id,
name,
concept_key,
is_builtin,
is_unique,
body,
where,
pre,
post,
ret,
definition,
definition_type,
desc,
autouse,
bound_body,
props or {},
variables or (),
parameters or (),
)
digest = self.compute_metadata_digest(metadata)
if self.sheerka.om.exists_in_current(self.CONCEPTS_BY_HASH_ENTRY, digest):
already_defined = self.sheerka.om.get(self.CONCEPTS_BY_HASH_ENTRY, digest)
error = ErrorContext(self.NAME, context, ConceptAlreadyDefined(metadata, already_defined.id))
return ReturnValue(self.NAME, False, error)
metadata.digest = digest
metadata.all_attrs = self.compute_all_attrs(variables)
# bnf_expr = None
# if definition_type == DefinitionType.BNF:
# try:
# bnf_expr = self.compute_concept_bnf(definition)
# except InvalidBnf as ex:
# error = ErrorContext(self.NAME, context, ex)
# return ReturnValue(self.NAME, False, error)
# try:
# first_item_res = self.recompute_first_items(context, None, [metadata])
# except FirstItemError as ex:
# return ReturnValue(self.NAME, False, ex)
# at this point everything is fine. let's get the id and save everything
om = self.sheerka.om
metadata.id = str(self.sheerka.om.get(self.sheerka.OBJECTS_IDS_ENTRY, self.USER_CONCEPTS_IDS))
om.add_concept(metadata)
# self.update_first_items_caches(context, first_item_res)
# if bnf_expr:
# self.bnf_expr_cache.put(metadata.id, bnf_expr)
# # update references
# for ref in self.compute_references(bnf_expr):
# om.put(self.CONCEPTS_REFERENCES_ENTRY, ref, metadata.id)
return ReturnValue(self.NAME, True, self.newn(BuiltinConcepts.NEW_CONCEPT, metadata=metadata))
def newn(self, concept_name: str, **kwargs):
"""
new_by_name
Creates and returns an instance of a new concept by its name
:param concept_name:
:type concept_name:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
metadata = self.get_by_name(concept_name)
if metadata is NotFound:
return self._inner_new(self.get_by_name(BuiltinConcepts.UNKNOWN_CONCEPT), requested_name=concept_name)
if isinstance(metadata, list):
return [self._inner_new(m, **kwargs) for m in metadata]
return self._inner_new(metadata, **kwargs)
def newi(self, concept_id: str, **kwargs):
"""
new_by_id
Creates and returns an instance of a new concept by its id
:param concept_id:
:type concept_id:
:param kwargs:
:type kwargs:
:return:
:rtype:
"""
metadata = self.get_by_id(concept_id)
if metadata is NotFound:
return self._inner_new(self.get_by_name(BuiltinConcepts.UNKNOWN_CONCEPT), requested_id=concept_id)
return self._inner_new(metadata, **kwargs)
def get_by_name(self, key: str):
"""
Returns a concept metadata, using its name
:param key:
:type key:
:return:
:rtype:
"""
return self.sheerka.om.get(self.CONCEPTS_BY_NAME_ENTRY, key)
def get_by_id(self, concept_id: str):
"""
Returns a concept metadata, using its name
:param concept_id:
:type concept_id:
:return:
:rtype:
"""
return self.sheerka.om.get(self.CONCEPTS_BY_ID_ENTRY, concept_id)
def get_by_key(self, key: str):
"""
Returns a concept metadata, using its name
:param key:
:type key:
:return:
:rtype:
"""
return self.sheerka.om.get(self.CONCEPTS_BY_KEY_ENTRY, key)
@staticmethod
def compute_metadata_digest(metadata: ConceptMetadata):
"""
Compute once for all the digest of the definition of a concept
:param metadata:
:type metadata:
:return:
:rtype:
"""
as_dict = {p: getattr(metadata, p) for p in PROPERTIES_FOR_DIGEST}
return hashlib.sha256(f"{as_dict}".encode("utf-8")).hexdigest()
@staticmethod
def compute_all_attrs(variables: tuple | None):
"""
Compute the list of available attributes for a concept
:param variables:
:return:
:rtype:
"""
all_attrs = DefaultProps.copy()
if variables:
all_attrs += [k for k, v in variables]
return tuple(all_attrs)
@staticmethod
def compute_concept_bnf(definition):
pass
@staticmethod
def create_concept_key(name: str, definition: str | None, variables: tuple | None):
"""
Creates the key from the definition
:param name:
:type name:
:param definition:
:type definition:
:param variables:
:type variables:
:return:
:rtype:
"""
definition_to_use = definition or name
tokens = list(Tokenizer(definition_to_use, yield_eof=False))
if variables is None or len(strip_tokens(tokens, True)) == 1:
variables_to_use = []
else:
variables_to_use = [k for k, v in variables]
parts = []
for token in tokens:
if token.type == TokenKind.WHITESPACE:
continue
if token.value in variables_to_use:
parts.append(VARIABLE_PREFIX + str(variables_to_use.index(token.value)))
else:
parts.append(token.value)
return " ".join(parts)
def _create_builtin_concept(self, concept_id: int, name: str, desc: str, variables: tuple = ()):
variables_to_use = tuple((k, NotInit) for k in variables)
concept_key = self.create_concept_key(name, None, variables_to_use)
metadata = ConceptMetadata(
str(concept_id),
name,
concept_key,
True,
False,
"",
"",
"",
"",
"",
"",
DefinitionType.DEFAULT,
desc,
False,
variables[0] if variables else "",
{},
variables_to_use,
variables,
)
metadata.digest = self.compute_metadata_digest(metadata)
metadata.all_attrs = self.compute_all_attrs(variables_to_use)
self.sheerka.om.add_concept(metadata)
@staticmethod
def _inner_new(_metadata_def: ConceptMetadata, **kwargs):
concept = Concept(_metadata_def)
for k, v in kwargs.items():
concept.set_value(k, v)
return concept
+223
View File
@@ -0,0 +1,223 @@
from dataclasses import dataclass
from common.utils import to_dict
from core.ExecutionContext import ExecutionContext, ExecutionContextActions
from core.ReturnValue import ReturnValue
from core.services.BaseService import BaseService
from evaluators.base_evaluator import AllReturnValuesEvaluator, BaseEvaluator, OneReturnValueEvaluator
@dataclass
class EvaluationPlan:
sorted_priorities: list[int] # list of available priorities
evaluators: dict[int, list[BaseEvaluator]]
class SheerkaEngine(BaseService):
"""
This service is used to process user input
It is responsible to parse and evaluate the information
It also holds the rule engine
"""
NAME = "Engine"
def __init__(self, sheerka):
super().__init__(sheerka, order=15)
self.execution_plan = None # { ExecutionContextActions : { priority : [evaluators] }}
self.no_evaluation_plan = EvaluationPlan([], {})
def initialize(self):
self.execution_plan = self.compute_execution_plan(self.sheerka.evaluators.values())
self.sheerka.bind_service_method(self.NAME, self.execute, True)
def call_evaluators(self,
context: ExecutionContext,
return_values: list[ReturnValue],
step: ExecutionContextActions):
"""
Calls all evaluators defined for a given step
:param context:
:type context:
:param return_values:
:type return_values:
:param step:
:type step:
:return:
:rtype:
"""
plan = self.get_evaluation_plan(context, step)
iteration = 0
while True:
with context.push(self.NAME,
ExecutionContextActions.EVALUATING_ITERATION,
{"step": step, "iteration": iteration},
desc=f"iteration #{iteration}") as iteration_context:
simple_digest = return_values.copy()
iteration_context.add_inputs(return_values=simple_digest)
for priority in plan.sorted_priorities:
return_values_copy = return_values.copy()
new_return_values = {}
return_values_to_delete = set()
for evaluator in plan.evaluators[priority]:
sub_context_desc = f"Evaluating using {evaluator.name} ({priority=})"
with iteration_context.push(self.NAME,
step,
{"step": step,
"iteration": iteration,
"evaluator": evaluator.name},
desc=sub_context_desc) as evaluator_context:
evaluator_context.add_inputs(return_values=return_values_copy)
# process evaluators that work on one simple return value at the time
if isinstance(evaluator, OneReturnValueEvaluator):
self.call_one_return_value_evaluator(evaluator_context,
evaluator,
return_values_copy,
new_return_values,
return_values_to_delete)
# process evaluators that work on all return values
else:
self.call_all_return_values_evaluator(evaluator_context,
evaluator,
return_values_copy,
new_return_values,
return_values_to_delete)
# Recreate the new return_value
# Try to keep the order of what replaces what
return_values = []
for item in return_values_copy:
if item not in return_values_to_delete:
return_values.append(item)
if item in new_return_values:
return_values.extend(new_return_values[item])
iteration_context.add_values(return_values=return_values.copy())
iteration += 1
if simple_digest == return_values:
# I can use a variable like 'has_changed', but I think that this comparison is explicit
# It explains that I stay in the loop if something was modified
break
return return_values
def execute(self,
context: ExecutionContext,
return_values: list[ReturnValue],
steps: list[ExecutionContextActions]):
"""
Runs the processing engine on the return_values
:param context:
:type context:
:param return_values:
:type return_values:
:param steps:
:type steps:
:return:
:rtype:
"""
for step in steps:
copy = return_values.copy()
with context.push(self.NAME, ExecutionContextActions.EVALUATING_STEP, {"step": step}) as sub_context:
sub_context.add_inputs(return_values=copy)
return_values = self.call_evaluators(sub_context, return_values, step)
sub_context.add_values(return_values=return_values)
sub_context.add_values(has_changed=(copy != return_values))
return return_values
def get_evaluation_plan(self, context: ExecutionContext, step: ExecutionContextActions) -> EvaluationPlan:
if step not in self.execution_plan:
return self.no_evaluation_plan
evaluators = self.execution_plan[step]
return EvaluationPlan(sorted(evaluators.keys(), reverse=True), evaluators)
@staticmethod
def call_one_return_value_evaluator(context: ExecutionContext,
evaluator: OneReturnValueEvaluator,
return_values: list[ReturnValue],
new_return_values: dict[ReturnValue, list[ReturnValue]],
return_values_to_delete: set[ReturnValue]):
"""
:param context:
:type context:
:param evaluator:
:type evaluator:
:param return_values:
:type return_values:
:param new_return_values:
:type new_return_values:
:param return_values_to_delete:
:type return_values_to_delete:
:return:
:rtype:
"""
context_trace = []
for item in return_values:
debug = {"item": item}
context_trace.append(debug)
m = evaluator.matches(context, item)
debug["match"] = m.status
if m.status:
result = evaluator.eval(context, m.obj, item)
return_values_to_delete.update(result.eaten)
new_return_values.setdefault(item, []).extend(result.new)
debug["new"] = result.new
debug["eaten"] = result.eaten
context.add_values(evaluation=context_trace)
@staticmethod
def call_all_return_values_evaluator(context: ExecutionContext,
evaluator: AllReturnValuesEvaluator,
return_values: list[ReturnValue],
new_return_values: dict[ReturnValue, list[ReturnValue]],
return_values_to_delete: set[ReturnValue]):
"""
:param context:
:type context:
:param evaluator:
:type evaluator:
:param return_values:
:type return_values:
:param new_return_values:
:type new_return_values:
:param return_values_to_delete:
:type return_values_to_delete:
:return:
:rtype:
"""
debug = {}
m = evaluator.matches(context, return_values)
debug["match"] = m.status
if m.status:
result = evaluator.eval(context, m.obj, return_values)
return_values_to_delete.update(result.eaten)
new_return_values.setdefault(result.new[0].parents[0], []).extend(result.new)
debug["new"] = result.new
debug["eaten"] = result.eaten
context.add_values(evaluation=debug)
@staticmethod
def compute_execution_plan(evaluators):
evaluators = [e for e in evaluators if e.enabled]
by_step = to_dict(evaluators, lambda e: e.step)
for k, v in by_step.items():
by_step[k] = to_dict(v, lambda e: e.priority)
return by_step
View File
-32
View File
@@ -1,32 +0,0 @@
def get_class(qname):
"""
Loads a class from its full qualified name
:param qname:
:return:
"""
parts = qname.split('.')
module = ".".join(parts[:-1])
m = __import__(module)
for comp in parts[1:]:
m = getattr(m, comp)
return m
def get_full_qualified_name(obj):
"""
Returns the full qualified name of a class (including its module name )
:param obj:
:return:
"""
if obj.__class__ == type:
module = obj.__module__
if module is None or module == str.__class__.__module__:
return obj.__name__ # Avoid reporting __builtin__
else:
return module + '.' + obj.__name__
else:
module = obj.__class__.__module__
if module is None or module == str.__class__.__module__:
return obj.__class__.__name__ # Avoid reporting __builtin__
else:
return module + '.' + obj.__class__.__name__