# -*- coding: utf-8 -*-
#
# Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved.
# 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
# http://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.
"""
This module contains the application manager
"""
import logging
import copy
from .components import DialogueManager, NaturalLanguageProcessor, QuestionAnswerer
from .components._config import get_max_history_len
from .components.dialogue import DialogueResponder
from .components.request import FrozenParams, Params, Request
from .resource_loader import ResourceLoader
logger = logging.getLogger(__name__)
[docs]def freeze_params(params):
"""
If params is a dictionary or Params we convert it into FrozenParams.
Otherwise we raise a TypeError.
Args:
params (dict, Params): The input params to convert
Returns:
FrozenParams: The converted params object
"""
params = params or FrozenParams()
if isinstance(params, dict):
params = FrozenParams(**params)
elif params.__class__ == Params:
params = FrozenParams(**dict(params))
elif not isinstance(params, FrozenParams):
raise TypeError(
"Invalid type for params argument. "
"Should be dict or {}".format(FrozenParams.__name__)
)
return params
[docs]class ApplicationManager:
"""The Application Manager is the core orchestrator of the MindMeld platform. It receives \
a client request, and processes that request by passing it through all the necessary \
components of MindMeld. Once processing is complete, the application manager returns \
the final response back to the client.
Attributes:
async_mode (bool): Whether the application is asynchronous or synchronous.
nlp (NaturalLanguageProcessor): The natural language processor.
question_answerer (QuestionAnswerer): The question answerer.
request_class (Request): Any class that inherits \
from Request
responder_class (DialogueResponder): Any class \
that inherits from the DialogueResponder
dialogue_manager (DialogueManager): The application's dialogue manager.
"""
MAX_HISTORY_LEN = 100
"""The max number of turns in history."""
def __init__(
self,
app_path,
nlp=None,
question_answerer=None,
es_host=None,
request_class=None,
responder_class=None,
text_preparation_pipeline=None,
async_mode=False,
):
self.async_mode = async_mode
self._app_path = app_path
# If NLP or QA were passed in, use the resource loader from there
if nlp:
resource_loader = nlp.resource_loader
if question_answerer:
question_answerer.resource_loader = resource_loader
elif question_answerer:
resource_loader = question_answerer.resource_loader
else:
resource_loader = ResourceLoader.create_resource_loader(
app_path, text_preparation_pipeline=text_preparation_pipeline
)
self._query_factory = resource_loader.query_factory
self.nlp = nlp or NaturalLanguageProcessor(app_path, resource_loader)
self.question_answerer = question_answerer or QuestionAnswerer(
app_path, resource_loader, es_host
)
self.request_class = request_class or Request
self.responder_class = responder_class or DialogueResponder
self.dialogue_manager = DialogueManager(
self.responder_class, async_mode=self.async_mode
)
self.max_history_len = (
get_max_history_len(self._app_path) or self.MAX_HISTORY_LEN
)
@property
def ready(self):
"""Whether the nlp component is ready."""
return self.nlp.ready
[docs] def load(self):
"""Loads all resources required to run a MindMeld application."""
if self.async_mode:
return self._load_async()
if self.nlp.ready:
# if we are ready, don't load again
return
self.nlp.load()
async def _load_async(self):
if self.nlp.ready:
# if we are ready, don't load again
return
# TODO: make an async nlp
self.nlp.load()
def _pre_dm(self, processed_query, context, params, frame, form, history):
# We pass in the previous turn's responder's params to the current request
# TODO: Currently, we serialize the form before passing it to the request and response
# since its hard to deserialize it.
request = self.request_class(
context=context,
history=history,
frame=frame,
form=form,
params=params,
**processed_query
)
# We reset the current turn's responder's params
response = self.responder_class(
frame=dict(frame),
form={},
params=Params(),
slots={},
history=copy.deepcopy(history),
request=request,
directives=[],
)
return request, response
[docs] def parse(
self, text, params=None, context=None, frame=None, form=None, history=None, verbose=False
):
"""
Args:
text (str): The text of the message sent by the user
params (Params/dict, optional): Contains parameters which modify how text is parsed
params.allowed_intents (list, optional): A list of allowed intents \
for model consideration
params.target_dialogue_state (str, optional): The target dialogue state
params.time_zone (str, optional): The name of an IANA time zone, such as \
'America/Los_Angeles', or 'Asia/Kolkata' \
See the [tz database](https://www.iana.org/time-zones) for more information.
params.timestamp (long, optional): A unix time stamp for the request (in seconds).
frame (dict, optional): A dictionary specifying the frame of the conversation
context (dict, optional): A dictionary of app-specific data
history (list, optional): A list of previous and current responder objects \
through interactions with MindMeld
verbose (bool, optional): Flag to return confidence scores for domains and intents
Returns:
TODO: Convert to dict
(Responder): A Responder object
.. _IANA tz database:
https://www.iana.org/time-zones
.. _List of tz database time zones:
https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
"""
if self.async_mode:
return self._parse_async(
text,
params=params,
context=context,
frame=frame,
form=form,
history=history,
verbose=verbose,
)
params = freeze_params(params)
history = history or []
frame = frame or {}
form = form or {}
context = context or {}
processed_query = self.nlp.process(query_text=text,
allowed_intents=params.allowed_intents,
locale=params.locale,
language=params.language,
time_zone=params.time_zone,
timestamp=params.timestamp,
dynamic_resource=params.dynamic_resource,
verbose=verbose)
request, response = self._pre_dm(
processed_query=processed_query,
context=context,
history=history,
frame=frame,
form=form,
params=params,
)
dm_responder = self.dialogue_manager.apply_handler(
request, response, target_dialogue_state=params.target_dialogue_state
)
modified_dm_responder = self._post_dm(dm_responder)
return modified_dm_responder
async def _parse_async(
self, text, params=None, context=None, frame=None, form=None, history=None, verbose=False
):
"""
Args:
text (str): The text of the message sent by the user
params (Params, optional): Contains parameters which modify how text is parsed
params.allowed_intents (list, optional): A list of allowed intents
for model consideration
params.target_dialogue_state (str, optional): The target dialogue state
params.time_zone (str, optional): The name of an IANA time zone, such as
'America/Los_Angeles', or 'Asia/Kolkata'
See the [tz database](https://www.iana.org/time-zones) for more information.
params.timestamp (long, optional): A unix time stamp for the request (in seconds).
context (dict, optional): A dictionary of app-specific data
history (list, optional): A list of previous and current responder objects
through interactions with MindMeld
verbose (bool, optional): Flag to return confidence scores for domains and intents
Returns:
@TODO: Convert to dict
(Responder): A Responder object
.. _IANA tz database:
https://www.iana.org/time-zones
.. _List of tz database time zones:
https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
"""
params = freeze_params(params)
context = context or {}
history = history or []
frame = frame or {}
form = form or {}
# TODO: make an async nlp
processed_query = self.nlp.process(query_text=text,
allowed_intents=params.allowed_intents,
locale=params.locale,
language=params.language,
time_zone=params.time_zone,
timestamp=params.timestamp,
dynamic_resource=params.dynamic_resource,
verbose=verbose)
request, response = self._pre_dm(
processed_query=processed_query,
context=context,
history=history,
frame=frame,
form=form,
params=params,
)
dm_responder = await self.dialogue_manager.apply_handler(
request, response, target_dialogue_state=params.target_dialogue_state
)
modified_dm_responder = self._post_dm(dm_responder)
return modified_dm_responder
def _post_dm(self, dm_response):
# Append this item to the history, but don't recursively store history
prev_request = dict(dm_response)
prev_request.pop("history", None)
prev_request["request"].pop("history", None)
# limit length of history
new_history = [prev_request, ] + dm_response.history
dm_response.history = new_history[: self.max_history_len]
return dm_response
[docs] def add_middleware(self, middleware):
"""Adds middleware for the dialogue manager.
Args:
middleware (callable): A dialogue manager middleware function
"""
self.dialogue_manager.add_middleware(middleware)
[docs] def add_dialogue_rule(self, name, handler, **kwargs):
"""Adds a dialogue rule for the dialogue manager.
Args:
name (str): The name of the dialogue state
handler (function): The dialogue state handler function
kwargs (dict): A list of options which specify the dialogue rule
"""
self.dialogue_manager.add_dialogue_rule(name, handler, **kwargs)