common_query_skill.py
Go to the documentation of this file.
1 # Copyright 2018 Mycroft AI Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 from enum import IntEnum
16 from abc import ABC, abstractmethod
17 from mycroft import MycroftSkill
18 from mycroft.messagebus.message import Message
19 
20 
21 class CQSMatchLevel(IntEnum):
22  EXACT = 1 # Skill could find a specific answer for the question
23  CATEGORY = 2 # Skill could find an answer from a category in the query
24  GENERAL = 3 # The query could be processed as a general quer
25 
26 
27 # Copy of CQSMatchLevel to use if the skill returns visual media
28 CQSVisualMatchLevel = IntEnum('CQSVisualMatchLevel',
29  [e.name for e in CQSMatchLevel])
30 
31 
32 def is_CQSVisualMatchLevel(match_level):
33  return isinstance(match_level, type(CQSVisualMatchLevel.EXACT))
34 
35 
36 VISUAL_DEVICES = ['mycroft_mark_2']
37 
38 
39 def handles_visuals(self, platform):
40  return platform in VISUAL_DEVICES
41 
42 
43 class CommonQuerySkill(MycroftSkill, ABC):
44  """ Question answering skills should be based on this class. The skill
45  author needs to implement `CQS_match_query_phrase` returning an answer
46  and can optionally implement `CQS_action` to perform additional actions
47  if the skill's answer is selected.
48 
49  This class works in conjunction with skill-query which collects
50  answers from several skills presenting the best one available.
51  """
52  def __init__(self, name=None, bus=None):
53  super().__init__(name, bus)
54 
55  def bind(self, bus):
56  """ Overrides the default bind method of MycroftSkill.
57 
58  This registers messagebus handlers for the skill during startup
59  but is nothing the skill author needs to consider.
60  """
61  if bus:
62  super().bind(bus)
63  self.add_event('question:query', self.__handle_question_query)
64  self.add_event('question:action', self.__handle_query_action)
65 
66  def __handle_question_query(self, message):
67  search_phrase = message.data["phrase"]
68 
69  # First, notify the requestor that we are attempting to handle
70  # (this extends a timeout while this skill looks for a match)
71  self.bus.emit(message.response({"phrase": search_phrase,
72  "skill_id": self.skill_id,
73  "searching": True}))
74 
75  # Now invoke the CQS handler to let the skill perform its search
76  result = self.CQS_match_query_phrase(search_phrase)
77 
78  if result:
79  match = result[0]
80  level = result[1]
81  answer = result[2]
82  callback = result[3] if len(result) > 3 else None
83  confidence = self.__calc_confidence(match, search_phrase, level)
84  self.bus.emit(message.response({"phrase": search_phrase,
85  "skill_id": self.skill_id,
86  "answer": answer,
87  "callback_data": callback,
88  "conf": confidence}))
89  else:
90  # Signal we are done (can't handle it)
91  self.bus.emit(message.response({"phrase": search_phrase,
92  "skill_id": self.skill_id,
93  "searching": False}))
94 
95  def __calc_confidence(self, match, phrase, level):
96  # Assume the more of the words that get consumed, the better the match
97  consumed_pct = len(match.split()) / len(phrase.split())
98  if consumed_pct > 1.0:
99  consumed_pct = 1.0
100 
101  # Add bonus if match has visuals and the device supports them.
102  platform = self.config_core.get('enclosure', {}).get('platform')
103  if is_CQSVisualMatchLevel(level) and handles_visuals(platform):
104  bonus = 0.1
105  else:
106  bonus = 0
107 
108  if int(level) == int(CQSMatchLevel.EXACT):
109  return 0.9 + (consumed_pct / 10) + bonus
110  elif int(level) == int(CQSMatchLevel.CATEGORY):
111  return 0.6 + (consumed_pct / 10) + bonus
112  elif int(level) == int(CQSMatchLevel.GENERAL):
113  return 0.5 + (consumed_pct / 10) + bonus
114  else:
115  return 0.0 # should never happen
116 
117  def __handle_query_action(self, message):
118  """ Message handler for question:action. Extracts phrase and data from
119  message forward this to the skills CQS_action method. """
120  if message.data["skill_id"] != self.skill_id:
121  # Not for this skill!
122  return
123  phrase = message.data["phrase"]
124  data = message.data.get("callback_data")
125  # Invoke derived class to provide playback data
126  self.CQS_action(phrase, data)
127 
128  @abstractmethod
129  def CQS_match_query_phrase(self, phrase):
130  """
131  Analyze phrase to see if it is a play-able phrase with this
132  skill. Needs to be implemented by the skill.
133 
134  Args:
135  phrase (str): User phrase uttered after "Play", e.g. "some music"
136 
137  Returns:
138  (match, CQSMatchLevel[, callback_data]) or None: Tuple containing
139  a string with the appropriate matching phrase, the PlayMatch
140  type, and optionally data to return in the callback if the
141  match is selected.
142  """
143  # Derived classes must implement this, e.g.
144  return None
145 
146  def CQS_action(self, phrase, data):
147  """
148  Take additional action IF the skill is selected.
149  The speech is handled by the common query but if the chosen skill
150  wants to display media, set a context or prepare for sending
151  information info over e-mail this can be implemented here.
152 
153  Args:
154  phrase (str): User phrase uttered after "Play", e.g. "some music"
155  data (dict): Callback data specified in match_query_phrase()
156  """
157  # Derived classes may implement this if they use additional media
158  # or wish to set context after being called.
159  pass
def get(phrase, lang=None, context=None)


mycroft_ros
Author(s):
autogenerated on Mon Apr 26 2021 02:35:40