dc6079821b
Docs Tests / Check for file changes (push) Has been cancelled
Docs Tests / Test Documentation (push) Has been cancelled
Docs Tests / Documentation Linting Checks (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.9) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.9) (push) Has been cancelled
Continuous Integration / Check for file changes (push) Has been cancelled
Continuous Integration / Wait for docs tests (push) Has been cancelled
Continuous Integration / Code Quality (push) Has been cancelled
Continuous Integration / Check for changelog (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Upload coverage reports to codeclimate (push) Has been cancelled
Continuous Integration / Run Non-Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Run Broker Integration Tests (push) Has been cancelled
Continuous Integration / Run Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Build Docker base images and setup environment (push) Has been cancelled
Continuous Integration / Build Docker (default) (push) Has been cancelled
Continuous Integration / Build Docker (full) (push) Has been cancelled
Continuous Integration / Build Docker (mitie-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-de) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-it) (push) Has been cancelled
Continuous Integration / Deploy to PyPI (push) Has been cancelled
Continuous Integration / Notify Slack & Publish Release Notes (push) Has been cancelled
Publish Documentation / Evaluate release tag (push) Has been cancelled
Publish Documentation / Prebuild Docs (push) Has been cancelled
Publish Documentation / Preview Docs (push) Has been cancelled
Publish Documentation / Check for file changes (push) Has been cancelled
Publish Documentation / Publish Docs (push) Has been cancelled
Automatic PR Merger / mergepal (push) Has been cancelled
CI Github Actions / Run Tests (push) Has been cancelled
Semgrep / Semgrep Workflow Security Scan (push) Has been cancelled
454 lines
16 KiB
Python
454 lines
16 KiB
Python
import logging
|
|
from abc import ABC, abstractmethod
|
|
from typing import Any, Dict, List, Optional, Text, Type
|
|
|
|
import rasa.shared.core.constants
|
|
from rasa.shared.exceptions import RasaException
|
|
import rasa.shared.utils.common
|
|
import rasa.shared.utils.io
|
|
from rasa.shared.constants import DOCS_URL_SLOTS
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class InvalidSlotTypeException(RasaException):
|
|
"""Raised if a slot type is invalid."""
|
|
|
|
|
|
class InvalidSlotConfigError(RasaException, ValueError):
|
|
"""Raised if a slot's config is invalid."""
|
|
|
|
|
|
class Slot(ABC):
|
|
"""Key-value store for storing information during a conversation."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def type_name(self) -> Text:
|
|
"""Name of the type of slot."""
|
|
...
|
|
|
|
def __init__(
|
|
self,
|
|
name: Text,
|
|
mappings: List[Dict[Text, Any]],
|
|
initial_value: Any = None,
|
|
value_reset_delay: Optional[int] = None,
|
|
influence_conversation: bool = True,
|
|
) -> None:
|
|
"""Create a Slot.
|
|
|
|
Args:
|
|
name: The name of the slot.
|
|
initial_value: The initial value of the slot.
|
|
mappings: List containing slot mappings.
|
|
value_reset_delay: After how many turns the slot should be reset to the
|
|
initial_value. This is behavior is currently not implemented.
|
|
influence_conversation: If `True` the slot will be featurized and hence
|
|
influence the predictions of the dialogue polices.
|
|
"""
|
|
self.name = name
|
|
self.mappings = mappings
|
|
self._value = initial_value
|
|
self.initial_value = initial_value
|
|
self._value_reset_delay = value_reset_delay
|
|
self.influence_conversation = influence_conversation
|
|
self._has_been_set = False
|
|
|
|
def feature_dimensionality(self) -> int:
|
|
"""How many features this single slot creates.
|
|
|
|
Returns:
|
|
The number of features. `0` if the slot is unfeaturized. The dimensionality
|
|
of the array returned by `as_feature` needs to correspond to this value.
|
|
"""
|
|
if not self.influence_conversation:
|
|
return 0
|
|
|
|
return self._feature_dimensionality()
|
|
|
|
def _feature_dimensionality(self) -> int:
|
|
"""See the docstring for `feature_dimensionality`."""
|
|
return 1
|
|
|
|
def has_features(self) -> bool:
|
|
"""Indicate if the slot creates any features."""
|
|
return self.feature_dimensionality() != 0
|
|
|
|
def value_reset_delay(self) -> Optional[int]:
|
|
"""After how many turns the slot should be reset to the initial_value.
|
|
|
|
If the delay is set to `None`, the slot will keep its value forever."""
|
|
# TODO: FUTURE this needs to be implemented - slots are not reset yet
|
|
return self._value_reset_delay
|
|
|
|
def as_feature(self) -> List[float]:
|
|
if not self.influence_conversation:
|
|
return []
|
|
|
|
return self._as_feature()
|
|
|
|
@abstractmethod
|
|
def _as_feature(self) -> List[float]:
|
|
raise NotImplementedError(
|
|
"Each slot type needs to specify how its "
|
|
"value can be converted to a feature. Slot "
|
|
"'{}' is a generic slot that can not be used "
|
|
"for predictions. Make sure you add this "
|
|
"slot to your domain definition, specifying "
|
|
"the type of the slot. If you implemented "
|
|
"a custom slot type class, make sure to "
|
|
"implement `.as_feature()`."
|
|
"".format(self.name)
|
|
)
|
|
|
|
def reset(self) -> None:
|
|
"""Resets the slot's value to the initial value."""
|
|
self.value = self.initial_value
|
|
self._has_been_set = False
|
|
|
|
@property
|
|
def value(self) -> Any:
|
|
"""Gets the slot's value."""
|
|
return self._value
|
|
|
|
@value.setter
|
|
def value(self, value: Any) -> None:
|
|
"""Sets the slot's value."""
|
|
self._value = value
|
|
self._has_been_set = True
|
|
|
|
@property
|
|
def has_been_set(self) -> bool:
|
|
"""Indicates if the slot's value has been set."""
|
|
return self._has_been_set
|
|
|
|
def __str__(self) -> Text:
|
|
return f"{self.__class__.__name__}({self.name}: {self.value})"
|
|
|
|
def __repr__(self) -> Text:
|
|
return f"<{self.__class__.__name__}({self.name}: {self.value})>"
|
|
|
|
@staticmethod
|
|
def resolve_by_type(type_name: Text) -> Type["Slot"]:
|
|
"""Returns a slots class by its type name."""
|
|
for cls in rasa.shared.utils.common.all_subclasses(Slot):
|
|
if cls.type_name == type_name:
|
|
return cls
|
|
try:
|
|
return rasa.shared.utils.common.class_from_module_path(type_name)
|
|
except (ImportError, AttributeError):
|
|
raise InvalidSlotTypeException(
|
|
f"Failed to find slot type, '{type_name}' is neither a known type nor "
|
|
f"user-defined. If you are creating your own slot type, make "
|
|
f"sure its module path is correct. "
|
|
f"You can find all build in types at {DOCS_URL_SLOTS}"
|
|
)
|
|
|
|
def persistence_info(self) -> Dict[str, Any]:
|
|
"""Returns relevant information to persist this slot."""
|
|
return {
|
|
"type": rasa.shared.utils.common.module_path_from_instance(self),
|
|
"initial_value": self.initial_value,
|
|
"influence_conversation": self.influence_conversation,
|
|
"mappings": self.mappings,
|
|
}
|
|
|
|
def fingerprint(self) -> Text:
|
|
"""Returns a unique hash for the slot which is stable across python runs.
|
|
|
|
Returns:
|
|
fingerprint of the slot
|
|
"""
|
|
data = {"slot_name": self.name, "slot_value": self.value}
|
|
data.update(self.persistence_info())
|
|
return rasa.shared.utils.io.get_dictionary_fingerprint(data)
|
|
|
|
|
|
class FloatSlot(Slot):
|
|
"""A slot storing a float value."""
|
|
|
|
type_name = "float"
|
|
|
|
def __init__(
|
|
self,
|
|
name: Text,
|
|
mappings: List[Dict[Text, Any]],
|
|
initial_value: Optional[float] = None,
|
|
value_reset_delay: Optional[int] = None,
|
|
max_value: float = 1.0,
|
|
min_value: float = 0.0,
|
|
influence_conversation: bool = True,
|
|
) -> None:
|
|
"""Creates a FloatSlot.
|
|
|
|
Raises:
|
|
InvalidSlotConfigError, if the min-max range is invalid.
|
|
UserWarning, if initial_value is outside the min-max range.
|
|
"""
|
|
super().__init__(
|
|
name, mappings, initial_value, value_reset_delay, influence_conversation
|
|
)
|
|
self.max_value = max_value
|
|
self.min_value = min_value
|
|
|
|
if min_value >= max_value:
|
|
raise InvalidSlotConfigError(
|
|
"Float slot ('{}') created with an invalid range "
|
|
"using min ({}) and max ({}) values. Make sure "
|
|
"min is smaller than max."
|
|
"".format(self.name, self.min_value, self.max_value)
|
|
)
|
|
|
|
if initial_value is not None and not (min_value <= initial_value <= max_value):
|
|
rasa.shared.utils.io.raise_warning(
|
|
f"Float slot ('{self.name}') created with an initial value "
|
|
f"{self.value}. This value is outside of the configured min "
|
|
f"({self.min_value}) and max ({self.max_value}) values."
|
|
)
|
|
|
|
def _as_feature(self) -> List[float]:
|
|
try:
|
|
capped_value = max(self.min_value, min(self.max_value, float(self.value)))
|
|
if abs(self.max_value - self.min_value) > 0:
|
|
covered_range = abs(self.max_value - self.min_value)
|
|
else:
|
|
covered_range = 1
|
|
return [1.0, (capped_value - self.min_value) / covered_range]
|
|
except (TypeError, ValueError):
|
|
return [0.0, 0.0]
|
|
|
|
def persistence_info(self) -> Dict[Text, Any]:
|
|
"""Returns relevant information to persist this slot."""
|
|
d = super().persistence_info()
|
|
d["max_value"] = self.max_value
|
|
d["min_value"] = self.min_value
|
|
return d
|
|
|
|
def _feature_dimensionality(self) -> int:
|
|
return len(self.as_feature())
|
|
|
|
|
|
class BooleanSlot(Slot):
|
|
"""A slot storing a truth value."""
|
|
|
|
type_name = "bool"
|
|
|
|
def _as_feature(self) -> List[float]:
|
|
try:
|
|
if self.value is not None:
|
|
return [1.0, float(bool_from_any(self.value))]
|
|
else:
|
|
return [0.0, 0.0]
|
|
except (TypeError, ValueError):
|
|
# we couldn't convert the value to float - using default value
|
|
return [0.0, 0.0]
|
|
|
|
def _feature_dimensionality(self) -> int:
|
|
return len(self.as_feature())
|
|
|
|
|
|
def bool_from_any(x: Any) -> bool:
|
|
"""Converts bool/float/int/str to bool or raises error."""
|
|
if isinstance(x, bool):
|
|
return x
|
|
elif isinstance(x, (float, int)):
|
|
return x == 1.0
|
|
elif isinstance(x, str):
|
|
if x.isnumeric():
|
|
return float(x) == 1.0
|
|
elif x.strip().lower() == "true":
|
|
return True
|
|
elif x.strip().lower() == "false":
|
|
return False
|
|
else:
|
|
raise ValueError("Cannot convert string to bool")
|
|
else:
|
|
raise TypeError("Cannot convert to bool")
|
|
|
|
|
|
class TextSlot(Slot):
|
|
type_name = "text"
|
|
|
|
def _as_feature(self) -> List[float]:
|
|
return [1.0 if self.value is not None else 0.0]
|
|
|
|
|
|
class ListSlot(Slot):
|
|
type_name = "list"
|
|
|
|
def _as_feature(self) -> List[float]:
|
|
try:
|
|
if self.value is not None and len(self.value) > 0:
|
|
return [1.0]
|
|
else:
|
|
return [0.0]
|
|
except (TypeError, ValueError):
|
|
# we couldn't convert the value to a list - using default value
|
|
return [0.0]
|
|
|
|
# FIXME: https://github.com/python/mypy/issues/8085
|
|
@Slot.value.setter # type: ignore[attr-defined,misc]
|
|
def value(self, value: Any) -> None:
|
|
"""Sets the slot's value."""
|
|
if value and not isinstance(value, list):
|
|
# Make sure we always store list items
|
|
value = [value]
|
|
|
|
# Call property setter of superclass
|
|
# FIXME: https://github.com/python/mypy/issues/8085
|
|
super(ListSlot, self.__class__).value.fset(self, value) # type: ignore[attr-defined] # noqa: E501
|
|
|
|
|
|
class CategoricalSlot(Slot):
|
|
"""Slot type which can be used to branch conversations based on its value."""
|
|
|
|
type_name = "categorical"
|
|
|
|
def __init__(
|
|
self,
|
|
name: Text,
|
|
mappings: List[Dict[Text, Any]],
|
|
values: Optional[List[Any]] = None,
|
|
initial_value: Any = None,
|
|
value_reset_delay: Optional[int] = None,
|
|
influence_conversation: bool = True,
|
|
) -> None:
|
|
"""Creates a `Categorical Slot` (see parent class for detailed docstring)."""
|
|
super().__init__(
|
|
name, mappings, initial_value, value_reset_delay, influence_conversation
|
|
)
|
|
if values and None in values:
|
|
rasa.shared.utils.io.raise_warning(
|
|
f"Categorical slot '{self.name}' has `null` listed as a possible value"
|
|
f" in the domain file, which translates to `None` in Python. This value"
|
|
f" is reserved for when the slot is not set, and should not be listed"
|
|
f" as a value in the slot's definition."
|
|
f" Rasa will ignore `null` as a possible value for the '{self.name}'"
|
|
f" slot. Consider changing this value in your domain file to, for"
|
|
f" example, `unset`, or provide the value explicitly as a string by"
|
|
f' using quotation marks: "null".',
|
|
category=UserWarning,
|
|
)
|
|
self.values = (
|
|
[str(v).lower() for v in values if v is not None] if values else []
|
|
)
|
|
|
|
def add_default_value(self) -> None:
|
|
"""Adds the special default value to the list of possible values."""
|
|
values = set(self.values)
|
|
if rasa.shared.core.constants.DEFAULT_CATEGORICAL_SLOT_VALUE not in values:
|
|
self.values.append(
|
|
rasa.shared.core.constants.DEFAULT_CATEGORICAL_SLOT_VALUE
|
|
)
|
|
|
|
def persistence_info(self) -> Dict[Text, Any]:
|
|
"""Returns serialized slot."""
|
|
d = super().persistence_info()
|
|
d["values"] = [
|
|
value
|
|
for value in self.values
|
|
# Don't add default slot when persisting it.
|
|
# We'll re-add it on the fly when creating the domain.
|
|
if value != rasa.shared.core.constants.DEFAULT_CATEGORICAL_SLOT_VALUE
|
|
]
|
|
return d
|
|
|
|
def _as_feature(self) -> List[float]:
|
|
r = [0.0] * self.feature_dimensionality()
|
|
|
|
# Return the zero-filled array if the slot is unset (i.e. set to None).
|
|
# Conceptually, this is similar to the case when the featurisation process
|
|
# fails, hence the returned features here are the same as for that case.
|
|
if self.value is None:
|
|
return r
|
|
|
|
try:
|
|
for i, v in enumerate(self.values):
|
|
if v == str(self.value).lower():
|
|
r[i] = 1.0
|
|
break
|
|
else:
|
|
if (
|
|
rasa.shared.core.constants.DEFAULT_CATEGORICAL_SLOT_VALUE
|
|
in self.values
|
|
):
|
|
i = self.values.index(
|
|
rasa.shared.core.constants.DEFAULT_CATEGORICAL_SLOT_VALUE
|
|
)
|
|
r[i] = 1.0
|
|
else:
|
|
rasa.shared.utils.io.raise_warning(
|
|
f"Categorical slot '{self.name}' is set to a value "
|
|
f"('{self.value}') "
|
|
"that is not specified in the domain. "
|
|
"Value will be ignored and the slot will "
|
|
"behave as if no value is set. "
|
|
"Make sure to add all values a categorical "
|
|
"slot should store to the domain."
|
|
)
|
|
except (TypeError, ValueError):
|
|
logger.exception("Failed to featurize categorical slot.")
|
|
return r
|
|
return r
|
|
|
|
def _feature_dimensionality(self) -> int:
|
|
return len(self.values)
|
|
|
|
|
|
class AnySlot(Slot):
|
|
"""Slot which can be used to store any value.
|
|
|
|
Users need to create a subclass of `Slot` in case
|
|
the information is supposed to get featurized.
|
|
"""
|
|
|
|
type_name = "any"
|
|
|
|
def __init__(
|
|
self,
|
|
name: Text,
|
|
mappings: List[Dict[Text, Any]],
|
|
initial_value: Any = None,
|
|
value_reset_delay: Optional[int] = None,
|
|
influence_conversation: bool = False,
|
|
) -> None:
|
|
"""Creates an `Any Slot` (see parent class for detailed docstring).
|
|
|
|
Raises:
|
|
InvalidSlotConfigError, if slot is featurized.
|
|
"""
|
|
if influence_conversation:
|
|
raise InvalidSlotConfigError(
|
|
f"An {AnySlot.__name__} cannot be featurized. "
|
|
f"Please use a different slot type for slot '{name}' instead. If you "
|
|
f"need to featurize a data type which is not supported out of the box, "
|
|
f"implement a custom slot type by subclassing '{Slot.__name__}'. "
|
|
f"See the documentation for more information: {DOCS_URL_SLOTS}"
|
|
)
|
|
|
|
super().__init__(
|
|
name, mappings, initial_value, value_reset_delay, influence_conversation
|
|
)
|
|
|
|
def __eq__(self, other: Any) -> bool:
|
|
"""Compares object with other object."""
|
|
if not isinstance(other, AnySlot):
|
|
return NotImplemented
|
|
|
|
return (
|
|
self.name == other.name
|
|
and self.initial_value == other.initial_value
|
|
and self._value_reset_delay == other._value_reset_delay
|
|
and self.value == other.value
|
|
)
|
|
|
|
def _as_feature(self) -> List[float]:
|
|
raise InvalidSlotConfigError(
|
|
f"An {AnySlot.__name__} cannot be featurized. "
|
|
f"Please use a different slot type for slot '{self.name}' instead. If you "
|
|
f"need to featurize a data type which is not supported out of the box, "
|
|
f"implement a custom slot type by subclassing '{Slot.__name__}'. "
|
|
f"See the documentation for more information: {DOCS_URL_SLOTS}"
|
|
)
|