from __future__ import annotations from typing import Any, Dict, Optional, Text, Type import dataclasses import uuid from rasa.engine.caching import Cacheable, TrainingCache from rasa.engine.graph import ExecutionContext, GraphComponent, SchemaNode from rasa.engine.storage.resource import Resource from rasa.engine.storage.storage import ModelStorage from rasa.engine.training import fingerprinting class PrecomputedValueProvider(GraphComponent): """Holds the precomputed values of a `GraphNode` from a previous training. Pre-computed values can either be - values loaded from cache - values which were provided during the fingerprint run by input nodes """ def __init__(self, output: Cacheable): """Initializes a `PrecomputedValueProvider`. Args: output: The precomputed output to return. """ self._output = output @classmethod def create( cls, config: Dict[Text, Any], model_storage: ModelStorage, resource: Resource, execution_context: ExecutionContext, ) -> PrecomputedValueProvider: """Creates instance (see parent class for full docstring).""" return cls(output=config["output"]) def get_value(self) -> Cacheable: """Returns the precomputed output.""" return self._output @classmethod def replace_schema_node(cls, node: SchemaNode, output: Any) -> None: """Updates a `SchemaNode` to use a `PrecomputedValueProvider`. This is for when we want to use the precomputed output value of a node from a previous training in a subsequent training. We replace the class in the `uses` of the node to a be a `PrecomputedValueProvider` configured to return the precomputed value. Args: node: The node to update. output: precomputed cached output that the `PrecomputedValueProvider` will return. """ node.uses = cls node.config = {"output": output} node.fn = cls.get_value.__name__ node.constructor_name = cls.create.__name__ @dataclasses.dataclass class FingerprintStatus: """Holds the output of a `FingerprintComponent` and is used to prune the graph. Attributes: output_fingerprint: A fingerprint of the node's output value. is_hit: `True` if node's fingerprint key exists in the cache, `False` otherwise. """ output_fingerprint: Optional[Text] is_hit: bool def fingerprint(self) -> Text: """Returns the internal fingerprint. If there is no fingerprint returns a random string that will never match. """ return self.output_fingerprint or uuid.uuid4().hex class FingerprintComponent(GraphComponent): """Replaces non-input nodes during a fingerprint run.""" def __init__( self, cache: TrainingCache, config_of_replaced_component: Dict[Text, Any], class_of_replaced_component: Type, ) -> None: """Initializes a `FingerprintComponent`. Args: cache: Training cache used to determine if the run is a hit or not. config_of_replaced_component: Needed to generate the fingerprint key. class_of_replaced_component: Needed to generate the fingerprint key. """ self._cache = cache self._config_of_replaced_component = config_of_replaced_component self._class_of_replaced_component = class_of_replaced_component @classmethod def create( cls, config: Dict[Text, Any], model_storage: ModelStorage, resource: Resource, execution_context: ExecutionContext, ) -> FingerprintComponent: """Creates a `FingerprintComponent` (see parent class for full docstring).""" return cls( cache=config["cache"], config_of_replaced_component=config["config_of_replaced_component"], class_of_replaced_component=config["graph_component_class"], ) def run(self, **kwargs: Any) -> FingerprintStatus: """Calculates the fingerprint key to determine if cached output can be used. If the fingerprint key matches an entry in the cache it means that there has been a previous node execution which matches the same component class, component config and input values. This means that we can potentially prune this node from the schema, or replace it with a cached value before the next graph run. Args: **kwargs: Inputs from all parent nodes. Returns: A `FingerprintStatus` determining if the run was a hit, and if it was a hit also the output fingerprint from the cache. """ fingerprint_key = fingerprinting.calculate_fingerprint_key( graph_component_class=self._class_of_replaced_component, config={ **self._class_of_replaced_component.get_default_config(), **self._config_of_replaced_component, }, inputs=kwargs, ) output_fingerprint = self._cache.get_cached_output_fingerprint(fingerprint_key) return FingerprintStatus( is_hit=output_fingerprint is not None, output_fingerprint=output_fingerprint ) @classmethod def replace_schema_node(cls, node: SchemaNode, cache: TrainingCache) -> None: """Updates a `SchemaNode` to use a `FingerprintComponent`. This is for when we want to do a fingerprint run. During the fingerprint run we replace all non-input nodes with `FingerprintComponent`s so we can determine whether they are able to be pruned or cached before the next graph run without running the actual components. Args: node: The node to update. cache: The cache is needed to determine of there is cache hit for the fingerprint key. """ graph_component_class = node.uses node.uses = cls # We update the node to be "eager" so that `FingerprintComponent.run` sees # ALL the inputs to the node. If it was not eager, we would miss any args used # by the constructor. node.eager = True node.constructor_name = cls.create.__name__ node.fn = cls.run.__name__ node.config = { "config_of_replaced_component": node.config, "cache": cache, "graph_component_class": graph_component_class, }