Add auto-reload upon file timestamp change
MODIFY
rasa_sdk/__main__.py
rasa_sdk/__main__.py
@@ -18,6 +18,7 @@ def main_from_args(args):
args.ssl_certificate,
args.ssl_keyfile,
args.ssl_password,
+ args.auto_reload,
)

MODIFY
rasa_sdk/cli/arguments.py
rasa_sdk/cli/arguments.py
@@ -49,3 +49,8 @@ def add_endpoint_arguments(parser):
help="If your ssl-keyfile is protected by a password, you can specify it "
"using this paramer.",
)
+ parser.add_argument(
+ "--auto-reload",
+ help="Enable auto-reloading of modules containing Action subclasses.",
+ action="store_true",
+ )

MODIFY
rasa_sdk/endpoint.py
rasa_sdk/endpoint.py
@@ -59,6 +59,7 @@ def create_argument_parser():
def create_app(
action_package_name: Union[Text, types.ModuleType],
cors_origins: Union[Text, List[Text], None] = "*",
+ auto_reload: bool = False,
) -> Sanic:
app = Sanic(__name__, configure_logging=False)
@@ -82,6 +83,10 @@ def create_app(
return response.json(body, status=400)
utils.check_version_compatibility(action_call.get("version"))
+
+ if auto_reload:
+ executor.reload()
+
try:
result = await executor.run(action_call)
except ActionExecutionRejection as e:
@@ -98,6 +103,9 @@ def create_app(
@app.get("/actions")
async def actions(_) -> HTTPResponse:
"""List all registered actions."""
+ if auto_reload:
+ executor.reload()
+
body = [{"name": k} for k in executor.actions.keys()]
return response.json(body, status=200)
@@ -111,9 +119,12 @@ def run(
ssl_certificate: Optional[Text] = None,
ssl_keyfile: Optional[Text] = None,
ssl_password: Optional[Text] = None,
+ auto_reload: bool = False,
):
logger.info("Starting action endpoint server...")
- app = create_app(action_package_name, cors_origins=cors_origins)
+ app = create_app(
+ action_package_name, cors_origins=cors_origins, auto_reload=auto_reload
+ )
ssl_context = create_ssl_context(ssl_certificate, ssl_keyfile, ssl_password)
app.run("0.0.0.0", port, ssl=ssl_context, workers=utils.number_of_sanic_workers())

MODIFY
rasa_sdk/executor.py
rasa_sdk/executor.py
@@ -7,14 +7,12 @@ from typing import Text, List, Dict, Any, Type, Union, Callable, Optional
import typing
import types
import sys
+import os
-from rasa_sdk.interfaces import Tracker, ActionNotFoundException
+from rasa_sdk.interfaces import Tracker, ActionNotFoundException, Action
from rasa_sdk import utils
-if typing.TYPE_CHECKING:
- from rasa_sdk.interfaces import Action
-
logger = logging.getLogger(__name__)
@@ -146,16 +144,25 @@ class CollectingDispatcher:
class ActionExecutor:
def __init__(self):
self.actions = {}
+ self._modules = {}
def register_action(self, action: Union[Type["Action"], "Action"]) -> None:
- from rasa_sdk.interfaces import Action
-
if inspect.isclass(action):
if action.__module__.startswith("rasa."):
logger.warning(f"Skipping built in Action {action}.")
return
else:
+ if getattr(action, "_sdk_loaded", False):
+ # This Action subclass has already been loaded in the past;
+ # do not try to load it again as either 1) it is already
+ # loaded or 2) it has been replaced by a newer version
+ # already.
+ return
+
+ # Mark the class as "loaded"
+ action._sdk_loaded = True
action = action()
+
if isinstance(action, Action):
self.register_function(action.name(), action.run)
else:
@@ -166,7 +173,6 @@ class ActionExecutor:
)
def register_function(self, name: Text, f: Callable) -> None:
- logger.info(f"Registered function for '{name}'.")
valid_keys = utils.arguments_of(f)
if len(valid_keys) < 3:
raise Exception(
@@ -176,6 +182,12 @@ class ActionExecutor:
f"tracker, domain. Your function accepts only {len(valid_keys)} "
"parameters."
)
+
+ if name in self.actions:
+ logger.info(f"Re-registered function for '{name}'.")
+ else:
+ logger.info(f"Registered function for '{name}'.")
+
self.actions[name] = f
def _import_submodules(
@@ -189,27 +201,38 @@ class ActionExecutor:
:rtype: dict[str, types.ModuleType]
"""
if isinstance(package, str):
- package = importlib.import_module(package)
+ package = self._import_module(package)
if not getattr(package, "__path__", None):
return
- results = {}
for loader, name, is_pkg in pkgutil.walk_packages(package.__path__):
full_name = package.__name__ + "." + name
- results[full_name] = importlib.import_module(full_name)
+ self._import_module(full_name)
+
if recursive and is_pkg:
self._import_submodules(full_name)
- def register_package(self, package: Union[Text, types.ModuleType]) -> None:
- from rasa_sdk.interfaces import Action
+ def _import_module(self, name: Text) -> types.ModuleType:
+ """TODO
+ """
+ module = importlib.import_module(name)
+
+ timestamp = os.path.getmtime(module.__file__)
+ self._modules[module.__file__] = (timestamp, module)
+ return module
+
+ def register_package(self, package: Union[Text, types.ModuleType]) -> None:
try:
self._import_submodules(package)
except ImportError:
logger.exception(f"Failed to register package '{package}'.")
sys.exit(1)
+ self._register_all_actions()
+
+ def _register_all_actions(self) -> None:
actions = utils.all_subclasses(Action)
for action in actions:
@@ -224,6 +247,40 @@ class ActionExecutor:
):
self.register_action(action)
+ def reload(self) -> None:
+ to_reload = {}
+
+ for path, (timestamp, module) in self._modules.items():
+ try:
+ new_timestamp = os.path.getmtime(path)
+ except OSError:
+ # Ignore missing files
+ continue
+
+ if new_timestamp > timestamp:
+ to_reload[path] = (new_timestamp, module)
+
+ module_reloaded = False
+
+ for path, (timestamp, module) in to_reload.items():
+ try:
+ new_module = importlib.reload(module)
+ self._modules[path] = (timestamp, new_module)
+ logger.info(
+ f"Reloaded module/package: '{module.__name__}' "
+ f"(file: '{os.path.relpath(path)}')"
+ )
+ module_reloaded = True
+ except (SyntaxError, ImportError):
+ logger.exception(
+ f"Error while reloading module/package: '{module.__name__}' "
+ f"(file: '{os.path.relpath(path)}'):"
+ )
+ logger.info("Please fix the error(s) in the Python file and try again.")
+
+ if module_reloaded:
+ self._register_all_actions()
+
@staticmethod
def _create_api_response(
events: List[Dict[Text, Any]], messages: List[Dict[Text, Any]]

Add auto-reload upon file timestamp change