src/dfcx_scrapi/builders/flows.py (338 lines of code) (raw):
"""A set of builder methods to create CX proto resource objects"""
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from dataclasses import dataclass
from typing import List, Union
from google.cloud.dialogflowcx_v3beta1.types import (
EventHandler,
Flow,
NluSettings,
TransitionRoute,
)
from dfcx_scrapi.builders.builders_common import BuildersCommon
from dfcx_scrapi.builders.fulfillments import FulfillmentBuilder
from dfcx_scrapi.builders.routes import (
EventHandlerBuilder,
TransitionRouteBuilder,
)
# logging config
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
class FlowBuilder(BuildersCommon):
"""Base Class for CX Flow builder."""
_proto_type = Flow
_proto_type_str = "Flow"
def __str__(self) -> str:
"""String representation of the proto_obj."""
self._check_proto_obj_attr_exist()
return (
f"Basic Information:\n{'='*25}\n{self._show_basic_info()}"
f"\n\n\nTransitionRoutes:\n{'='*25}"
f"\n{self._show_transition_routes()}"
f"\n\n\nEventHandlers:\n{'='*25}\n{self._show_event_handlers()}"
f"\n\n\nTransitoinRouteGroups:\n{'='*25}"
f"\n{self._show_transition_route_groups()}")
def _show_basic_info(self) -> str:
"""String representation for the basic information of proto_obj."""
self._check_proto_obj_attr_exist()
nlu_settings_str = (
f"\tModel type: {self.proto_obj.nlu_settings.model_type.name}"
"\n\tClassification threshold:"
f" {self.proto_obj.nlu_settings.classification_threshold}"
"\n\tTraining mode:"
f" {self.proto_obj.nlu_settings.model_training_mode.name}"
)
return (
f"display_name: {self.proto_obj.display_name}"
f"\ndescription:\n\t{self.proto_obj.description}"
f"\nNLU settings:\n{nlu_settings_str}"
)
def _show_transition_routes(self) -> str:
"""String representation for the transition routes of proto_obj."""
self._check_proto_obj_attr_exist()
return "\n".join([
f"TransitionRoute {i+1}:\n{str(TransitionRouteBuilder(tr))}"
f"\n{'*'*20}\n"
for i, tr in enumerate(self.proto_obj.transition_routes)
])
def _show_event_handlers(self) -> str:
"""String representation for the event handlers of proto_obj."""
self._check_proto_obj_attr_exist()
return "\n".join([
f"EventHandler {i+1}:\n{str(EventHandlerBuilder(eh))}\n{'*'*20}\n"
for i, eh in enumerate(self.proto_obj.event_handlers)
])
def _show_transition_route_groups(self) -> str:
"""String representation for the transition route groups of proto_obj"""
self._check_proto_obj_attr_exist()
return "\n".join([
f"TransitionRouteGroup {i+1}: {trg_id}"
for i, trg_id in enumerate(self.proto_obj.transition_route_groups)
])
def show_flow_info(
self, mode: str = "whole"
) -> None:
"""Show the proto_obj information.
Args:
mode (str):
Specifies what part of the page to show.
Options:
['basic', 'whole',
'routes' or 'transition routes',
'route groups' or 'transition route groups',
'events' or 'event handlers'
]
"""
self._check_proto_obj_attr_exist()
if mode == "basic":
print(self._show_basic_info())
elif mode in ["routes", "transition routes"]:
print(self._show_transition_routes())
elif mode in ["route groups", "transition route groups"]:
print(self._show_transition_route_groups())
elif mode in ["events", "event handlers"]:
print(self._show_event_handlers())
elif mode == "whole":
print(self)
else:
raise ValueError(
"mode should be in"
"['basic', 'whole',"
" 'routes', 'transition routes',"
" 'route groups', 'transition route groups',"
" 'events', 'event handlers']"
)
def show_stats(self) -> None:
"""Provide some stats about the Page."""
self._check_proto_obj_attr_exist()
stats_instance = FlowStats(self.proto_obj)
stats_instance.generate_stats()
def create_new_proto_obj(
self,
display_name: str,
description: str = None,
overwrite: bool = False
) -> Flow:
"""Create a new Flow.
Args:
display_name (str):
Required. The human-readable name of the flow.
description (str):
The description of the flow. The maximum length is 500 characters.
If exceeded, the request is rejected.
overwrite (bool)
Overwrite the new proto_obj if proto_obj already contains a Flow.
Returns:
A Flow object stored in proto_obj.
"""
# Types error checking
if not(display_name and isinstance(display_name, str)):
raise ValueError("`display_name` should be a nonempty string.")
if (
description
and not isinstance(description, str)
and len(description) > 500
):
raise ValueError(
"`description` should be a string and"
" it's length should be less than 500 characters."
)
# `overwrite` parameter error checking
if self.proto_obj and not overwrite:
raise UserWarning(
"proto_obj already contains a Flow."
" If you wish to overwrite it, pass `overwrite` as True."
)
# Create the Flow
if overwrite or not self.proto_obj:
self.proto_obj = Flow(
display_name=display_name,
description=description
)
# Set the NLU settings to default
self.nlu_settings()
return self.proto_obj
def nlu_settings(
self,
model_type: int = 1,
classification_threshold: float = 0.3,
model_training_mode: int = 1
) -> Flow:
"""NLU related settings of the flow.
Args:
model_type (int):
Indicates the type of NLU model:
1 = MODEL_TYPE_STANDARD, 3 = MODEL_TYPE_ADVANCED
classification_threshold (float):
To filter out false positive results and
still get variety in matched natural language
inputs for your agent, you can tune the machine
learning classification threshold. If the
returned score value is less than the threshold
value, then a no-match event will be triggered.
The score values range from 0.0 (completely
uncertain) to 1.0 (completely certain). If set
to 0.0, the default of 0.3 is used.
model_training_mode (int):
Indicates NLU model training mode:
1 = MODEL_TRAINING_MODE_AUTOMATIC
2 = MODEL_TRAINING_MODE_MANUAL
Returns:
A Flow object stored in proto_obj.
"""
self._check_proto_obj_attr_exist()
# Type error checking
if model_type not in [1, 3]:
raise ValueError(
"`model_type` should be in [1, 3]."
"\n1: MODEL_TYPE_STANDARD"
"\n3: MODEL_TYPE_ADVANCED"
)
if model_training_mode not in [1, 2]:
raise ValueError(
"`model_training_mode` should be in [1, 2]."
"\n1: MODEL_TRAINING_MODE_AUTOMATIC"
"\n2: MODEL_TRAINING_MODE_MANUAL"
)
if not(
isinstance(classification_threshold, float)
and (0 < classification_threshold < 1)
):
raise ValueError(
"`classification_threshold` should be a float"
" range from 0.0 to 1.0."
)
the_nlu_settings = NluSettings(
model_type=model_type,
model_training_mode=model_training_mode,
classification_threshold=classification_threshold
)
self.proto_obj.nlu_settings = the_nlu_settings
return self.proto_obj
def add_transition_route(
self,
transition_routes: Union[TransitionRoute, List[TransitionRoute]]
) -> Flow:
"""Add single or multiple TransitionRoutes to the Flow.
Args:
transition_routes (TransitionRoute | List[TransitionRoute]):
A single or list of TransitionRoutes to add
to the Flow existing in proto_obj.
Returns:
A Flow object stored in proto_obj.
"""
self._check_proto_obj_attr_exist()
# Type error checking
self._is_type_or_list_of_types(
transition_routes, TransitionRoute, "transition_routes"
)
if not isinstance(transition_routes, list):
transition_routes = [transition_routes]
self.proto_obj.transition_routes.extend(transition_routes)
return self.proto_obj
def add_event_handler(
self,
event_handlers: Union[EventHandler, List[EventHandler]]
) -> Flow:
"""Add single or multiple EventHandlers to the Flow.
Args:
event_handlers (EventHandler | List[EventHandler]):
A single or list of EventHandler to add
to the Flow existing in proto_obj.
Returns:
A Flow object stored in proto_obj.
"""
self._check_proto_obj_attr_exist()
# Type error checking
self._is_type_or_list_of_types(
event_handlers, EventHandler, "event_handlers"
)
if not isinstance(event_handlers, list):
event_handlers = [event_handlers]
self.proto_obj.event_handlers.extend(event_handlers)
return self.proto_obj
def add_transition_route_group(
self,
transition_route_groups: Union[str, List[str]]
) -> Flow:
"""Add single or multiple TransitionRouteGroups to the Flow.
Args:
transition_route_groups (str | List[str]):
A single or list of TransitionRouteGroup's id to add
to the Flow existed in proto_obj. Format:
``projects/<Project ID>/locations/<Location ID>/agents/<Agent ID>/
flows/<Flow ID>/transitionRouteGroups/<TransitionRouteGroup ID>``.
Returns:
A Flow object stored in proto_obj.
"""
self._check_proto_obj_attr_exist()
# Type error checking
self._is_type_or_list_of_types(
transition_route_groups, str, "transition_route_groups"
)
if not isinstance(transition_route_groups, list):
transition_route_groups = [transition_route_groups]
self.proto_obj.transition_route_groups.extend(transition_route_groups)
return self.proto_obj
def remove_transition_route(
self,
transition_route: TransitionRoute = None,
intent: str = None,
condition: str = None
) -> Flow:
"""Remove a transition route from the Flow.
At least one of the `transition_route`, `intent`, or `condition` should
be specfied.
Args:
transition_route (TransitionRoute):
The TransitionRoute to remove from the Flow.
intent (str):
TransitionRoute's intent that should be removed from the Flow.
condition (str):
TransitionRoute's condition that should be removed from the Flow.
Returns:
A Flow object stored in proto_obj.
"""
self._check_proto_obj_attr_exist()
new_routes = []
for tr in self.proto_obj.transition_routes:
if self._match_transition_route(
transition_route=tr, target_route=transition_route,
intent=intent, condition=condition
):
continue
new_routes.append(tr)
self.proto_obj.transition_routes = new_routes
return self.proto_obj
def remove_event_handler(
self,
event_handlers: Union[EventHandler, List[EventHandler]] = None,
event_names: Union[str, List[str]] = None
) -> Flow:
"""Remove single or multiple EventHandlers from the Flow.
Args:
event_handlers (EventHandler | List[EventHandler]):
A single or list of EventHandler to remove
from the Flow existing in proto_obj.
Only one of the `event_handlers` and
`event_names` should be specified.
event_names (str | List[str]):
A single or list of EventHandler's event names corresponding to the
EventHandler to remove from the Flow existing in proto_obj.
Only one of the `event_handlers` and
`event_names` should be specified.
Returns:
A Flow object stored in proto_obj.
"""
self._check_proto_obj_attr_exist()
if event_handlers and event_names:
raise UserWarning(
"Only one of the `event_handlers` and "
"`event_names` should be specified."
)
if event_handlers:
new_ehs = self._find_unmatched_event_handlers(event_handlers)
elif event_names:
new_ehs = self._find_unmatched_event_handlers_by_name(event_names)
else:
raise UserWarning(
"At least one of the `event_handlers` and "
"`event_names` should be specified."
)
self.proto_obj.event_handlers = new_ehs
return self.proto_obj
def remove_transition_route_group(
self,
transition_route_groups: Union[str, List[str]]
) -> Flow:
"""Remove single or multiple TransitionRouteGroups from the Flow.
Args:
transition_route_groups (str | List[str]):
A single or list of TransitionRouteGroup's id to remove
from the Flow existing in proto_obj. Format:
``projects/<Project ID>/locations/<Location ID>/agents/<Agent ID>/
flows/<Flow ID>/transitionRouteGroups/<TransitionRouteGroup ID>``.
Returns:
A Flow object stored in proto_obj.
"""
self._check_proto_obj_attr_exist()
# Type error checking
self._is_type_or_list_of_types(
transition_route_groups, str, "transition_route_groups"
)
if not isinstance(transition_route_groups, list):
transition_route_groups = [transition_route_groups]
new_trgs = [
trg
for trg in self.proto_obj.transition_route_groups
if trg not in transition_route_groups
]
self.proto_obj.transition_route_groups = new_trgs
return self.proto_obj
@dataclass
class FlowStats():
"""A class for tracking the stats of CX Flow object."""
flow_proto_obj: Flow
# Transition Routes
transition_routes_count: int = 0
routes_with_fulfill_count: int = 0
routes_with_webhook_fulfill_count: int = 0
intent_routes_count: int = 0
cond_routes_count: int = 0
intent_and_cond_routes_count: int = 0
# Event Handlers
event_handlers_count: int = 0
events_with_fulfill_count: int = 0
events_with_webhook_fulfill_count: int = 0
# Transition Route Groups
transition_route_groups_count: int = 0
def calc_transition_route_stats(self):
"""Calculating TransitionRoute related stats"""
self.transition_routes_count = len(
self.flow_proto_obj.transition_routes
)
for tr in self.flow_proto_obj.transition_routes:
if tr.trigger_fulfillment:
self.routes_with_fulfill_count += 1
fb = FulfillmentBuilder(tr.trigger_fulfillment)
if fb.has_webhook():
self.routes_with_webhook_fulfill_count += 1
if tr.intent and tr.condition:
self.intent_and_cond_routes_count += 1
elif tr.intent and not tr.condition:
self.intent_routes_count += 1
elif not tr.intent and tr.condition:
self.cond_routes_count += 1
def create_transition_route_str(self) -> str:
"""String representation of TransitionRoutes stats."""
transition_routes_str = (
f"# of Transition Routes: {self.transition_routes_count}"
)
routes_with_fulfill_str = (
f"# of routes with fulfillment: {self.routes_with_fulfill_count}"
)
routes_with_webhook_fulfill_str = (
"# of routes uses webhook for fulfillment:"
f" {self.routes_with_webhook_fulfill_count}"
)
intent_routes_str = f"# of intent routes: {self.intent_routes_count}"
cond_routes_str = f"# of condition routes: {self.cond_routes_count}"
intent_and_cond_routes_str = (
"# of intent and condition routes:"
f" {self.intent_and_cond_routes_count}"
)
return (
f"{transition_routes_str}\n\t{intent_routes_str}"
f"\n\t{cond_routes_str}\n\t{intent_and_cond_routes_str}"
f"\n\t{routes_with_fulfill_str}"
f"\n\t{routes_with_webhook_fulfill_str}"
)
def calc_event_handler_stats(self):
"""Calculating EventHandler related stats."""
self.event_handlers_count = len(self.flow_proto_obj.event_handlers)
for eh in self.flow_proto_obj.event_handlers:
fb = FulfillmentBuilder(eh.trigger_fulfillment)
if fb.has_webhook():
self.events_with_webhook_fulfill_count += 1
if eh.trigger_fulfillment:
self.events_with_fulfill_count += 1
def create_event_handler_str(self) -> str:
"""String representation of EventHandlers stats."""
event_handlers_str = f"# of Event Handlers: {self.event_handlers_count}"
events_with_fulfill_str = (
"# of Event Handlers with fulfillment:"
f" {self.events_with_fulfill_count}"
)
events_with_webhook_fulfill_str = (
"# of Event Handlers uses webhook for fulfillment:"
f" {self.events_with_webhook_fulfill_count}"
)
return (
f"{event_handlers_str}\n\t{events_with_fulfill_str}"
f"\n\t{events_with_webhook_fulfill_str}"
)
def create_transition_route_group_str(self) -> str:
"""String representation of TransitionRouteGroup stats."""
self.transition_route_groups_count = len(
self.flow_proto_obj.transition_route_groups
)
return (
"# of Transition Route Groups:"
f" {self.transition_route_groups_count}"
)
def generate_stats(self):
"""Generate stats for the Flow."""
self.calc_transition_route_stats()
self.calc_event_handler_stats()
routes_stats_str = self.create_transition_route_str()
events_stats_str = self.create_event_handler_str()
route_groups_stats_str = self.create_transition_route_group_str()
out = (
f"{routes_stats_str}\n{events_stats_str}\n{route_groups_stats_str}"
)
print(out)