Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve exception reporting for CLI commands #6856

Merged
merged 13 commits into from
Oct 2, 2020
10 changes: 10 additions & 0 deletions changelog/6856.improvement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Improved exception handling within Rasa Open Source.

All exceptions that are somewhat expected (e.g. errors in file formats like
configurations or training data) will share a common base class
`RasaOpenSourceException`.
tmbo marked this conversation as resolved.
Show resolved Hide resolved

::warning Backwards Incompatibility
Base class for the exception raised when an exception can not be found has been changed
tmbo marked this conversation as resolved.
Show resolved Hide resolved
from a `NameError` to a `ValueError`.
::
56 changes: 33 additions & 23 deletions rasa/__main__.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import sys
import argparse
import logging
import os
import platform
import sys
tmbo marked this conversation as resolved.
Show resolved Hide resolved

from rasa_sdk import __version__ as rasa_sdk_version

import rasa.utils.io
from rasa import version
import rasa.telemetry
from rasa.cli import (
scaffold,
run,
train,
data,
export,
interactive,
run,
scaffold,
shell,
telemetry,
test,
train,
visualize,
data,
x,
export,
)
from rasa.cli.arguments.default_arguments import add_logging_options
from rasa.cli.utils import parse_last_positional_argument_as_model_path
from rasa.utils.common import set_log_level, set_log_and_warnings_filters
from rasa.shared.exceptions import RasaOpenSourceException
from rasa.shared.utils.cli import print_error
import rasa.telemetry
from rasa.utils.common import set_log_and_warnings_filters, set_log_level
import rasa.utils.io
import rasa.utils.tensorflow.environment as tf_env
from rasa_sdk import __version__ as rasa_sdk_version

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -89,8 +93,6 @@ def print_version() -> None:

def main() -> None:
# Running as standalone python application
import os
import sys

parse_last_positional_argument_as_model_path()
arg_parser = create_argument_parser()
Expand All @@ -106,17 +108,25 @@ def main() -> None:
# insert current path in syspath so custom modules are found
sys.path.insert(1, os.getcwd())

if hasattr(cmdline_arguments, "func"):
rasa.utils.io.configure_colored_logging(log_level)
set_log_and_warnings_filters()
rasa.telemetry.initialize_error_reporting()
cmdline_arguments.func(cmdline_arguments)
elif hasattr(cmdline_arguments, "version"):
print_version()
else:
# user has not provided a subcommand, let's print the help
logger.error("No command specified.")
arg_parser.print_help()
try:
if hasattr(cmdline_arguments, "func"):
rasa.utils.io.configure_colored_logging(log_level)
set_log_and_warnings_filters()
rasa.telemetry.initialize_error_reporting()
cmdline_arguments.func(cmdline_arguments)
elif hasattr(cmdline_arguments, "version"):
print_version()
else:
# user has not provided a subcommand, let's print the help
logger.error("No command specified.")
arg_parser.print_help()
sys.exit(1)
except RasaOpenSourceException as e:
# these are exceptions we expect to happen (e.g. invalid training data format)
# it doesn't make sense to print a stacktrace for these if we are not in
# debug mode
logger.debug("Failed to run CLI command due to an exception.", exc_info=e)
print_error(f"{e.__class__.__name__}: {e}")
sys.exit(1)


Expand Down
4 changes: 1 addition & 3 deletions rasa/cli/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,7 @@ def run_nlu_test(args: argparse.Namespace) -> None:
for file in args.config:
try:
validation_utils.validate_yaml_schema(
rasa.shared.utils.io.read_file(file),
CONFIG_SCHEMA_FILE,
show_validation_errors=False,
rasa.shared.utils.io.read_file(file), CONFIG_SCHEMA_FILE,
)
config_files.append(file)
except validation_utils.InvalidYamlFileError:
Expand Down
3 changes: 2 additions & 1 deletion rasa/core/actions/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
ACTION_BACK_NAME,
REQUESTED_SLOT,
)
from rasa.shared.exceptions import RasaOpenSourceException
from rasa.shared.nlu.constants import INTENT_NAME_KEY, INTENT_RANKING_KEY
from rasa.shared.core.events import (
UserUtteranceReverted,
Expand Down Expand Up @@ -661,7 +662,7 @@ def name(self) -> Text:
return self._name


class ActionExecutionRejection(Exception):
class ActionExecutionRejection(RasaOpenSourceException):
"""Raising this exception will allow other policies
to predict a different action"""

Expand Down
8 changes: 3 additions & 5 deletions rasa/core/featurizers/tracker_featurizers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jsonpickle
import logging

from rasa.shared.exceptions import RasaOpenSourceException
from rasa.shared.nlu.constants import TEXT
from tqdm import tqdm
from typing import Tuple, List, Optional, Dict, Text, Union
Expand All @@ -22,18 +23,15 @@
logger = logging.getLogger(__name__)


class InvalidStory(Exception):
class InvalidStory(RasaOpenSourceException):
"""Exception that can be raised if story cannot be featurized."""

def __init__(self, message) -> None:
self.message = message
super(InvalidStory, self).__init__()

def __str__(self) -> Text:
# return message in error colours
return rasa.shared.utils.io.wrap_with_color(
self.message, color=rasa.shared.utils.io.bcolors.FAIL
)
return self.message


class TrackerFeaturizer:
Expand Down
17 changes: 13 additions & 4 deletions rasa/core/policies/ensemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import Text, Optional, Any, List, Dict, Tuple, Set, NamedTuple, Union

import rasa.core
from rasa.shared.exceptions import RasaOpenSourceException
import rasa.shared.utils.common
import rasa.shared.utils.io
import rasa.utils.io
Expand Down Expand Up @@ -380,7 +381,6 @@ def from_dict(cls, policy_configuration: Dict[Text, Any]) -> List[Policy]:
parsed_policies = []

for policy in policies:
policy_name = policy.pop("name")
if policy.get("featurizer"):
featurizer_func, featurizer_config = cls.get_featurizer_from_dict(
policy
Expand All @@ -401,6 +401,7 @@ def from_dict(cls, policy_configuration: Dict[Text, Any]) -> List[Policy]:
# override policy's featurizer with real featurizer class
policy["featurizer"] = featurizer_func(**featurizer_config)

policy_name = policy.pop("name")
try:
constr_func = registry.policy_from_module_path(policy_name)
try:
Expand All @@ -424,7 +425,11 @@ def from_dict(cls, policy_configuration: Dict[Text, Any]) -> List[Policy]:
def get_featurizer_from_dict(cls, policy) -> Tuple[Any, Any]:
# policy can have only 1 featurizer
if len(policy["featurizer"]) > 1:
raise InvalidPolicyConfig("policy can have only 1 featurizer")
raise InvalidPolicyConfig(
f"Every policy can only have 1 featurizer "
f"but '{policy.get('name')}' "
f"uses {len(policy['featurizer'])} featurizers."
)
featurizer_config = policy["featurizer"][0]
featurizer_name = featurizer_config.pop("name")
featurizer_func = registry.featurizer_from_module_path(featurizer_name)
Expand All @@ -435,7 +440,11 @@ def get_featurizer_from_dict(cls, policy) -> Tuple[Any, Any]:
def get_state_featurizer_from_dict(cls, featurizer_config) -> Tuple[Any, Any]:
# featurizer can have only 1 state featurizer
if len(featurizer_config["state_featurizer"]) > 1:
raise InvalidPolicyConfig("featurizer can have only 1 state featurizer")
raise InvalidPolicyConfig(
f"Every featurizer can only have 1 state "
f"featurizer but one of the featurizers uses "
f"{len(featurizer_config['state_featurizer'])}."
)
state_featurizer_config = featurizer_config["state_featurizer"][0]
state_featurizer_name = state_featurizer_config.pop("name")
state_featurizer_func = registry.state_featurizer_from_module_path(
Expand Down Expand Up @@ -737,7 +746,7 @@ def _check_policy_for_forms_available(
)


class InvalidPolicyConfig(Exception):
class InvalidPolicyConfig(RasaOpenSourceException):
"""Exception that can be raised when policy config is not valid."""

pass
16 changes: 7 additions & 9 deletions rasa/core/policies/rule_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json

from rasa.shared.constants import DOCS_URL_RULES
from rasa.shared.exceptions import RasaOpenSourceException
import rasa.shared.utils.io
from rasa.shared.core.events import FormValidation, UserUttered, ActionExecuted
from rasa.core.featurizers.tracker_featurizers import TrackerFeaturizer
Expand Down Expand Up @@ -56,20 +57,17 @@
DO_NOT_PREDICT_LOOP_ACTION = "do_not_predict_loop_action"


class InvalidRule(Exception):
class InvalidRule(RasaOpenSourceException):
"""Exception that can be raised when rules are not valid."""

def __init__(self, message: Text) -> None:
super().__init__()
self.message = message + (
f"\nYou can find more information about the usage of "
f"rules at {DOCS_URL_RULES}. "
)
self.message = message

def __str__(self) -> Text:
# return message in error colours
return rasa.shared.utils.io.wrap_with_color(
self.message, color=rasa.shared.utils.io.bcolors.FAIL
return self.message + (
f"\nYou can find more information about the usage of "
f"rules at {DOCS_URL_RULES}. "
tmbo marked this conversation as resolved.
Show resolved Hide resolved
)


Expand Down Expand Up @@ -370,7 +368,7 @@ def _find_contradicting_rules(
if error_messages:
error_messages = "\n".join(error_messages)
raise InvalidRule(
f"\nContradicting rules or stories found🚨\n\n{error_messages}\n"
f"\nContradicting rules or stories found 🚨\n\n{error_messages}\n"
f"Please update your stories and rules so that they don't contradict "
f"each other."
)
Expand Down
54 changes: 36 additions & 18 deletions rasa/nlu/components.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from collections import defaultdict
import itertools
import logging
import typing
from typing import Any, Dict, Hashable, List, Optional, Set, Text, Tuple, Type, Iterable

from rasa.shared.exceptions import RasaOpenSourceException
from rasa.shared.nlu.constants import TRAINABLE_EXTRACTORS
from rasa.nlu.config import RasaNLUModelConfig, override_defaults, InvalidConfigError
from rasa.shared.nlu.training_data.training_data import TrainingData
Expand All @@ -15,6 +17,10 @@
logger = logging.getLogger(__name__)


class MissingDependencyException(RasaOpenSourceException):
"""Raised if a python package dependency is needed, but not installed."""


def find_unavailable_packages(package_names: List[Text]) -> Set[Text]:
"""Tries to import all package names and returns the packages where it failed.

Expand Down Expand Up @@ -46,21 +52,32 @@ def validate_requirements(component_names: List[Text]) -> None:
from rasa.nlu import registry

# Validate that all required packages are installed
failed_imports = set()
failed_imports = {}
for component_name in component_names:
component_class = registry.get_component_class(component_name)
failed_imports.update(
find_unavailable_packages(component_class.required_packages())
unavailable_packages = find_unavailable_packages(
component_class.required_packages()
)
if unavailable_packages:
failed_imports[component_name] = unavailable_packages
if failed_imports: # pragma: no cover
# if available, use the development file to figure out the correct
# version numbers for each requirement
raise Exception(
f"Not all required importable packages are installed. "
dependency_component_map = defaultdict(list)
for component, missing_dependencies in failed_imports.items():
for dependency in missing_dependencies:
dependency_component_map[dependency].append(component)

missing_lines = [
f"{d} (needed for {', '.join(cs)})"
for d, cs in dependency_component_map.items()
]
missing = "\n - ".join(missing_lines)
raise MissingDependencyException(
f"Not all required importable packages are installed to use "
f"the configured NLU pipeline. "
f"To use this pipeline, you need to install the "
f"missing dependencies. "
f"Please install the package(s) that contain the module(s): "
f"{', '.join(failed_imports)}"
f"missing modules: \n"
f" - {missing}\n"
f"Please install the packages that contain the missing modules."
)


Expand All @@ -75,9 +92,7 @@ def validate_empty_pipeline(pipeline: List["Component"]) -> None:
raise InvalidConfigError(
"Can not train an empty pipeline. "
"Make sure to specify a proper pipeline in "
"the configuration using the 'pipeline' key. "
"The 'backend' configuration key is "
"NOT supported anymore."
"the configuration using the 'pipeline' key."
)


Expand All @@ -97,8 +112,9 @@ def validate_only_one_tokenizer_is_used(pipeline: List["Component"]) -> None:

if len(tokenizer_names) > 1:
raise InvalidConfigError(
f"More than one tokenizer is used: {tokenizer_names}. "
f"You can use only one tokenizer."
f"The pipeline configuration contains more than one tokenizer, "
f"which is not possible at this time. You can only use one tokenizer. "
f"The pipeline contained the following tokenizers: {tokenizer_names}. "
tmbo marked this conversation as resolved.
Show resolved Hide resolved
)


Expand Down Expand Up @@ -137,8 +153,10 @@ def validate_required_components(pipeline: List["Component"]) -> None:

if missing_components:
raise InvalidConfigError(
f"'{component.name}' requires {missing_components}. "
f"Add required components to the pipeline."
f"The pipeline configuration contains errors. The component "
f"'{component.name}' requires '{missing_components}' to be "
f"placed before it in the pipeline. Please "
f"add the required components to the pipeline."
)


Expand Down Expand Up @@ -283,7 +301,7 @@ def __str__(self) -> Text:
return self.message


class UnsupportedLanguageError(Exception):
class UnsupportedLanguageError(RasaOpenSourceException):
"""Raised when a component is created but the language is not supported.

Attributes:
Expand Down
3 changes: 2 additions & 1 deletion rasa/nlu/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import ruamel.yaml as yaml
from typing import Any, Dict, List, Optional, Text, Union

from rasa.shared.exceptions import RasaOpenSourceException
import rasa.shared.utils.io
import rasa.utils.io
from rasa.shared.constants import (
Expand All @@ -16,7 +17,7 @@
logger = logging.getLogger(__name__)


class InvalidConfigError(ValueError):
class InvalidConfigError(ValueError, RasaOpenSourceException):
tmbo marked this conversation as resolved.
Show resolved Hide resolved
"""Raised if an invalid configuration is encountered."""

def __init__(self, message: Text) -> None:
Expand Down
Loading