@@ -0,0 +1,7 @@
|
||||
class BuiltinConcepts:
|
||||
SHEERKA = "__SHEERKA"
|
||||
|
||||
NEW_CONCEPT = "__NEW_CONCEPT"
|
||||
UNKNOWN_CONCEPT = "__UNKNOWN_CONCEPT"
|
||||
USER_INPUT = "__USER_INPUT"
|
||||
PARSER_INPUT = "__PARSER_INPUT"
|
||||
@@ -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)))
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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__
|
||||
Reference in New Issue
Block a user