在本系列关于 Mycroft 开源语音助手的第 1 部分和第 2 部分中,我为学习如何创建技能奠定了基础。在第 3 部分中,我介绍了如何创建一个技能的大纲,并建议首先用纯 Python 创建技能,以确保方法按预期工作。这样,当出现问题时,您就知道问题与 Mycroft 技能的构建方式有关,而不是代码本身。
在本文中,您将通过添加以下内容来增强第 3 部分的大纲
- Mycroft 实体
- Padatious 意图
- Adapt 意图
- 对话框
- 对话上下文
该项目的代码可以在我的 GitLab 仓库中找到。
让我们开始吧!
改进您的技能
提醒一下,该项目的目的是使用 Mycroft 在 OurGroceries 应用程序中添加购物清单项。但是,本教程可以应用于各种家庭自动化应用,例如打开灯、获取早间天气预报或控制您的娱乐系统。
以下是目前技能大纲的样子
from mycroft import intent_file_handler, MycroftSkill, intent_handler
from mycroft.skills.context import adds_context, removes_context
class OurGroceriesSkill(MycroftSkill):
def __init__(self):
MycroftSkill.__init__(self)
# Mycroft should call this function directly when the user
# asks to create a new item
def create_item_on_list(self, message):
pass
# Mycroft should also call this function directly
def create_shopping_list(self, message):
pass
# This is not called directly, but instead should be triggered
# as part of context aware decisions
def handle_dont_create_anyways_context(self):
pass
# This function is also part of the context aware decision tree
def handle_create_anyways_context(self):
pass
def stop(self):
pass
def create_skill():
return OurGroceriesSkill()
就目前而言,Mycroft 将成功加载该技能,但它不会做任何事情,因为所有方法都包含命令 pass
。现在,忽略 __init__(self)
方法,开始使用 create_item_on_list
方法。从注释中,您可以看到意图是让 Mycroft 直接调用此方法。这意味着您需要声明一个意图。您该怎么做呢?
使用意图
您可能已经注意到,大纲 mycroft-msk
创建(在第三篇文章中)的 handle_test
方法上方有一个看起来很奇怪的函数 @intent_file_handler('ourgroceries.intent')
。这些是 Python 中的特殊符号,称为装饰器(如果您愿意,请访问 Real Python 了解 Python 装饰器入门)。对于本教程,只需知道装饰器是将您的函数传递到 Mycroft 开发的预构建函数中的一种方法就足够了。这节省了大量工作和样板代码。
回想一下本系列的第三部分,本项目使用了两个意图解析器:Padatious 和 Adapt,我在第二篇文章中对此进行了描述。
Padatious 意图
那么您如何知道使用哪个装饰器以及在哪里使用它呢?好问题!我将从 Padatious 开始,它更容易理解。如果您还记得第二篇文章,Padatious 技能是根据技能开发者认为与技能相关的短语进行训练的。由于 Mycroft 可能会安装许多使用 Padatious 意图引擎的技能,因此每个意图都会被 Mycroft 使用的神经网络模块评分。然后,Mycroft 选择得分最高的意图并执行其功能。
Mycroft 将用于训练意图的短语放在扩展名为 .intent
的文件中。您可以有多个 .intent
文件,但您必须显式引用每个文件。这意味着如果您有 create.item.intent
和 create.category.intent
,则不会混淆您的变量是从哪个文件填充的,因为您必须按文件名调用它们。正如您在 mycroft-msk
的输出中看到的那样,装饰器直观地命名为 @intent_file_handler()
。只需使用文件名作为装饰器的参数,例如 @intent_file_handler("create.item.intent")
。
想想人们可能会用哪些短语将商品添加到购物清单。由于此技能的动机是使用 Mycroft 创建购物清单,因此示例代码使用了与食物相关的术语,但您可以使用通用术语。话虽如此,以下是一些您可能会说出的将商品添加到购物清单的短语
- 将西红柿添加到我的购物清单
- 将西红柿添加到购物清单
- 将西红柿添加到 Costco 清单
您也可以选择使用一些语法不正确的短语,以解释 Mycroft 对用户声音的误解。从上面的列表中,哪些信息在编程上是相关的?tomatoes
、shopping list
、grocery list
和 Costco list
。官方文档将这种类型的对象称为实体。如果您觉得这样更容易理解,您可以将实体视为变量。当您创建意图文件时,这将变得更加清晰。虽然 mycroft-msk
命令默认会将意图放在 locale/en-us
下,但我将我的意图放在 vocab/en-us/
下。为什么?嗯,那是因为 Adapt 意图解析器将其文件存储在 vocab
中,我更喜欢将所有意图文件保存在同一位置。我的文件 vocab/en-us/create.item.intent
以
add {Food} to my {ShoppingList}
这定义了实体 Food
和 ShoppingList
。
重要提示:Padatious 实体不区分大小写,并且 Padatious 将所有内容解释为小写。例如,ShoppingList
将变为 shoppinglist
。
现在您有了一个意图,让 Mycroft 说出一个包含您的实体的短语。不要忘记添加意图装饰器!您的新函数将如下所示
@intent_file_handler("create.item.intent")
def create_item_on_list(self, message):
"""
This function adds an item to the specified list
:param message:
:return: Nothing
"""
item_to_add = message.data.get('food')
list_name = message.data.get('shoppinglist')
self.speak("Adding %s to %s" % (item_to_add, list_name))
下图使用了三个短语
- 将西红柿添加到我的购物清单
- 将钉子添加到我的五金清单
- 将小圆面包添加到杂货清单
Mycroft 将无法弄清楚其中一个短语背后的意图。您能猜出是哪个以及为什么吗?

(Steve Ovens,CC BY-SA 4.0)
如果视频速度有点快,这里是答案:Mycroft 无法处理短语 add buns to groceries list
,因为它缺少关键字 my
。意图明确指出 add {Food} to my {ShoppingList}
。如果没有用户输入的单词 my
,则技能的 Padatious 意图得分较低,因此 Mycroft 不会选择此技能来处理请求。最简单的解决方案是在您的意图文件中添加新行,如下所示
add {Food} to {ShoppingList}
当 Mycroft 检测到更改时,它通常可以重新加载技能,但我更喜欢重新启动 Mycroft 的技能部分以确保安全。我在测试期间也经常清除日志,所以我运行以下命令在一行中完成所有操作
./stop-mycroft.sh skills;sudo rm -f /var/log/mycroft/skills.log; ./start-mycroft.sh skills; mycroft-cli-client
Mycroft 重启后测试技能会产生以下结果
add buns to groceries list
>> Adding buns to groceries list
如果还不清楚,则 Mycroft 在 mycroft-cli-client
中做出的任何响应都以 >>
为前缀,以指示其响应。现在您有了一个基本意图,请返回并查看本系列第 3 部分中此技能的目标
- 登录/身份验证
- 获取当前购物清单的列表
- 将商品添加到特定购物清单
- 将商品添加到特定清单下的类别
- 能够添加类别(因为 OurGroceries 允许将商品放入类别中)
暂时忽略前两项——这些项涉及项目的在线部分,您需要先完成其他目标。对于第三项,您有一个基本意图,理论上,它应该能够获取 Mycroft 检测到的实体,并将它们转换为 Python 代码中的变量。对于列表中的第四项,在您的意图中添加两行新行
add {Food} to my {ShoppingList} under {Category}
add {Food} to {ShoppingList} under {Category}
您还需要稍微修改您的函数。当使用 Padatious 意图解析器时,实体通过 message.data.get()
函数返回。如果实体未定义,此函数将返回 None
。换句话说,如果 Mycroft 无法从用户发出的 utterance
中解析 {Category}
,则 message.data.get()
将返回 None
。考虑到这一点,这里有一些快速测试代码
@intent_file_handler("create.item.intent")
def create_item_on_list(self, message):
"""
This function adds an item to the specified list
:param message:
:return: Nothing
"""
item_to_add = message.data.get('food')
list_name = message.data.get('shoppinglist')
category_name = message.data.get('category')
if category_name is None:
self.speak("Adding %s to %s" % (item_to_add, list_name))
else:
self.speak("Adding %s to %s under the category %s" % (item_to_add, list_name, category_name))
这是一个测试这些代码更改的示例

(Steve Ovens,CC BY-SA 4.0)
在示例中,Mycroft 回应 >> Adding nails to my hardware list under
,但您告诉 Mycroft 说单词 under
的唯一时间是当 category_name
的值不是 None
时。这是因为意图解析器将单词 under
解释为实体 ShoppingList
的一部分。因为 utterance 中有单词 my
,所以与 utterance 匹配的句子可能是
add {Food} to my {ShoppingList}
或add {Food} to my {ShoppingList} under {Category}
由于用户没有说明 {Category}
,Mycroft 选择第一个语句作为最正确的。这意味着 my
之后的任何内容都将转换为实体 {ShoppingList}
。因此,由于 {Category}
是 None
,Mycroft 会说“Adding nails to my hardware list under”,而不是“Adding nails to my hardware list under None”。
Padatious 最初可能看起来有点简单。对于您需要 Mycroft 匹配的每个短语,只需在意图文件中添加一行即可。但是,对于复杂的意图,您可能有几十行尝试涵盖您要处理的所有不同 utterance。
还有另一种可能值得考虑的选择。Padatious 意图支持括号扩展。这意味着您可以使用 OR 语句的形式来减少意图中的行数。回到过去,该示例试图解释三种情况
add {Food} to my {ShoppingList}
add {Food} to my {ShoppingList} under {Category}
add {Food} to the {ShoppingList}
add {Food} to the {ShoppingList} under {Category}
add {Food} to {ShoppingList}
add {Food} to {ShoppingList} under {Category}
如果您想使用 OR 语句重写此内容以组合 my
和 the
关键字,您可以编写
add {Food} to ( my | the ) {ShoppingList}
add {Food} to ( my | the ) {ShoppingList} under {Category}
add {Food} to {ShoppingList}
add {Food} to {ShoppingList} under {Category}
这从意图中删除了两行。括号扩展还支持使某些内容成为可选的。因此,如果您想使 the
和 my
成为可选的,从而允许短语 add {Food} to {ShoppingList}
,它将如下所示
add {Food} to ( | my | the ) {ShoppingList}
add {Food} to ( | my | the ) {ShoppingList} under {Category}
这个简单的更改涵盖了所有三种情况(一旦您重新启动 Mycroft 技能子系统)。如果您愿意,您可以更进一步,将其浓缩为一行
add {Food} to ( | my | the ) {ShoppingList} ( | under {Category})
注意:为了更容易阅读,请在您的意图括号扩展中使用空格。
总结关于 Padatious 意图解析的要点
- 您必须给出几个短语示例,以便 Mycroft 提出正确的匹配项。
- Padatious 意图使用实体(例如
{Food}
)来识别可以从您的 Python 代码中检索的对象值。 - 实体始终为小写,无论您在意图文件中如何声明它们。
- 如果无法从 utterance 中解析实体,则其值为
None
。 - Padatious 意图的装饰器是
@intent_file_handler('my.intent.file.intent')
。
Adapt 意图
与 Padatious 意图(您在意图文件中指定实体)不同,Adapt 意图解析器使用一系列关键字,这些关键字与正则表达式 (regex) 文件结合使用,以尝试捕获实体。在以下情况下,您将使用 Adapt 而不是 Padatious
- 期望 utterance 是复杂的,并且需要更强大的 regex 解析
- 希望或需要 Mycroft 具有上下文感知能力
- 需要意图尽可能轻量级
也就是说,Adapt 使用的 voc
文件非常灵活。它们可以包含单个单词(如 官方文档 中所示),或者它们可以包含您想要响应的句子的开头。
由于本项目的目标之一是让 Mycroft 在 OurGroceries 应用程序中创建一个新的购物清单,我想添加一些基本的检查,以便在存在名称相似的清单时通知用户,并询问他们是否仍要创建新清单。这应该减少清单重复和商品错放的情况。
模拟一些代码,然后您可以处理 vocab 和 regex 文件。虽然您可以使用 Pytest 或类似的单元测试来断言特定值,但为了简单起见,您将创建一个名为“shopping list”的列表。Python 模拟函数将如下所示
def create_shopping_list(self, message):
fake_list = ["shopping list"]
self.new_shopping_list_name = message.data['ListName'].lower()
for current_shopping_list in fake_list:
try:
if self.new_shopping_list_name in current_shopping_list:
if self.new_shopping_list_name == current_shopping_list:
self.speak("The shopping list %s already exists" % self.new_shopping_list_name )
break
else:
self.speak("I found a similar naming list called %s" % current_shopping_list)
# This hands off to either handle_dont_create_anyways_context or handle_create_anyways_context
# to make a context aware decision
self.speak("Would you like me to add your new list anyways?", expect_response=True)
break
else:
self.speak("Ok creating a new list called %s" % self.new_shopping_list_name)
except Exception as ex:
print(ex)
pass
请注意,我正在使用 forloop
迭代 fake_list
。那是因为,从理论上讲,将从 OurGroceries 应用程序返回多个列表。另请注意 try/except
块;我对异常进行了常规传递,因为现在,我不知道可能会遇到哪些类型的异常。当您使用和调试代码时,您可以稍微收紧它。
另一个需要注意的行是
self.speak("Would you like me to add your new list anyways?", expect_response=True)
这段代码将使 Mycroft 提示用户做出响应并存储结果。我将在对话上下文部分详细讨论这段代码。
正则表达式、实体和 Adapt 意图
现在您有了一些伪代码,但您需要添加装饰器才能让 Mycroft 执行您的代码。您需要创建三个文件才能使其工作:两个 vocab 文件和一个 regex 文件。regex 文件,我将其命名为 add.shopping.list.rx
,如下所示
start a new list called (?P<ListName>.*)
create a new list called (?P<ListName>.*)
add a new list called (?P<ListName>.*)
您可以将其设为单行,但为了简单起见,请将其保留为三行。请注意这个看起来很奇怪的符号:(?P<ListName>.*)
。这是代码中捕获和创建实体的部分。在这种情况下,实体称为 ListName
。为了检查您的语法,我推荐 Pythex。当我调试我的 regex 时,它非常有用(我在 regex 方面很糟糕)。
重要提示:Adapt 意图区分大小写。
Adapt 和 vocab 文件
现在您的 regex 包含您期望的完整句子,创建您的两个 vocab 文件。第一个文件名为 CreateKeyword.voc
。正如您可以从文件名中推断出的那样,您想要与 create
操作关联的所有单词都应位于此处。此文件非常简单
start a new
create a new
add a new
在文档中,您通常会看到每行只有一个单词。但是,由于某些 Mycroft 默认技能使用 start
和 create
,我需要添加单词,以便 Mycroft 能够适当地选择我的技能。
第二个文件甚至更简单。它名为 ListKeyword.voc
,其中只有一个单词
list
定义了这些文件后,您现在可以构建您的装饰器
@intent_handler(IntentBuilder('CreateShoppingIntent').require('CreateKeyword').require('ListKeyword').require("ListName"))
IntentBuilder
中的第一个参数是 'CreateShoppingIntent'
;这是意图的名称,是完全可选的。如果您想将其留空,可以这样做。require
部分有点令人困惑。当涉及到关键字时,require
的参数是不带文件扩展名的文件名。在这种情况下,其中一个文件名为 ListKeyword.voc
,因此传递到 require
的参数只是 'ListKeyword'
。
虽然您可以随意命名您的 vocab 文件,但我强烈建议在文件中使用单词 Keyword
,这样当您构建 intent_handler
装饰器时,就可以清楚地知道您需要什么。
如果 require
实际上是来自 regex 文件的实体,则 require
的参数是您在 regex 中定义的实体的名称。如果您的 regex 是 start a new list called (?P<NewList>.*)
,那么您将编写 require('NewList')
。
重新启动 Mycroft 技能子部分并尝试一下。您应该在 Mycroft 命令行界面中看到此内容
add a new list called hardware
>> Ok creating a new list called hardware
create a new list called hardware
>> Ok creating a new list called hardware
start a new list called hardware
>> Ok creating a new list called hardware
对话上下文
太棒了,它工作了!现在将以下装饰器添加到您的函数中
@adds_context("CreateAnywaysContext")
此装饰器与 Mycroft 支持的对话上下文相关联。对话上下文本质上是您可以正常与 Mycroft 对话的地方,它会理解您的意思。例如,您可以问:“约翰·昆西·亚当斯是谁?” 在 Mycroft 回应后,说类似“约翰·昆西·亚当斯是美国第六任总统”,您可以问:“他当上总统时多大了?” 如果您首先问第二个问题,Mycroft 无法知道代词他指的是谁。但是,在此对话的上下文中,Mycroft 理解他指的是约翰·昆西·亚当斯。
回到创建对话上下文,其装饰器的参数是上下文的名称。此示例将上下文称为 CreateAnywaysContext
,因此完整的装饰器是 @adds_context("CreateAnywaysContext")
。此模拟方法现已完成。但是,您现在需要添加两个简单的方法来处理用户的反馈。您可以通过要求是或否答案来简化购物清单技能。创建 YesKeyword.voc
和 NoKeyword.voc
,并将单词 yes
和 no
分别放在其中。
现在在您的 Python 中创建另外两种方法
@intent_handler(IntentBuilder('DoNotAddIntent').require("NoKeyword").require('CreateAnywaysContext').build())
@removes_context("CreateAnywayscontext")
def handle_dont_create_anyways_context(self):
"""
Does nothing but acknowledges the user does not wish to proceed
Uses dont.add.response.dialog
:return:
"""
self.speak_dialog('dont.add.response')
@intent_handler(IntentBuilder('AddAnywaysIntent').require("YesKeyword").require('CreateAnywaysContext').build())
@removes_context("CreateAnywayscontext")
def handle_create_anyways_context(self):
"""
If the user wants to create a similarly named list, it is handled here
Uses do.add.response.dialog
:return:
"""
self.speak_dialog('do.add.response')
这里有两件事您到目前为止还没有见过
@remove_context
self.speak_dialog
如果调用了需要 CreateAnywaysContext
的方法,则装饰器 @remove_context
会删除上下文,以便 Mycroft 不会意外地多次执行上下文。虽然多个上下文可以应用于一个方法,但本项目不会使用它们。
对话框
对话框是包含 Mycroft 可以从中选择的多个预构建响应的文件。这些对话框存储在 dialog/{language tag}/
中,语言标记基于 IETF 标准。示例可以在 Venea.net 的 IETF LanguageTag 列中找到。
Mycroft 从指定对话框文件中的句子列表中随机选择。为什么要使用对话框文件而不是在 Python 中实现 self.speak
?答案很简单:当您创建和使用对话框文件时,您不必更改 Python 代码来支持其他语言。
例如,如果名为 dont.add.response.dialog
的对话框文件存在于 en-us
下,其内容如下
Ok... exiting
Gotcha I won't add it
Ok I'll disregard it
Make up your mind!
您还可以创建 de-de/dont.add.response.dialog
,其内容如下
Ok... Beenden
Erwischt Ich werde es nicht hinzufügen
Ok, ich werde es ignorieren.
Entscheiden Sie sich!
在您的 Python 代码中,您将使用 self.speak_dialog('dont.add.response')
随机选择 Mycroft 使用的答案之一。如果用户的 Mycroft 语言设置为德语,Mycroft 将自动选择正确的对话框并以德语而不是英语播放对话框。
为了总结本节,在 dialog/en-us
下创建两个文件。对于 dont.add.response.dialog
,使用与上述示例相同的内容。对于 do.add.response.dialog
,使用
Ok adding it now
Sure thing
Yup yup yup
在本项目中的这一点,您的树应该看起来像这样
├── dialog
│ └── en-us
│ ├── do.add.response.dialog
│ └── dont.add.response.dialog
├── __init__.py
├── regex
│ └── en-us
│ └── add.shopping.list.rx
└── vocab
└── en-us
├── create.item.intent
├── CreateKeyword.voc
└── ListKeyword.voc
请注意,我手动创建了这些文件。如果您使用了 mycroft-msk create
方法,您可能会有 locale
目录、settingsmeta.yaml
或其他工件。
总结
到目前为止,我们已经介绍了很多内容。您已经实现了 Padatious 意图解析器,从理论上讲,它可以将新商品添加到列表中,无论您是否将其放在类别下。您还使用了 Adapt 意图解析器来添加新类别。您使用了对话上下文来提示用户在类似列表已存在时进行确认。最后,您学习了对话框的概念,作为 Mycroft 向用户提供各种确认响应的一种方式。
目前,代码看起来像
from mycroft import intent_file_handler, MycroftSkill, intent_handler
from mycroft.skills.context import adds_context, removes_context
from adapt.intent import IntentBuilder
class OurGroceriesSkill(MycroftSkill):
def __init__(self):
MycroftSkill.__init__(self)
# Mycroft should call this function directly when the user
# asks to create a new item
@intent_file_handler("create.item.intent")
def create_item_on_list(self, message):
"""
This function adds an item to the specified list
:param message:
:return: Nothing
"""
item_to_add = message.data.get('food')
list_name = message.data.get('shoppinglist')
category_name = message.data.get('category')
if category_name is None:
self.speak("Adding %s to %s" % (item_to_add, list_name))
else:
self.speak("Adding %s to %s under the category %s" % (item_to_add, list_name, category_name))
# Mycroft should also call this function directly
@intent_handler(IntentBuilder('CreateShoppingIntent').require('CreateKeyword').require('ListKeyword').require("ListName"))
def create_shopping_list(self, message):
fake_list = ["shopping list"]
self.new_shopping_list_name = message.data['ListName'].lower()
for current_shopping_list in fake_list:
try:
if self.new_shopping_list_name in current_shopping_list:
if self.new_shopping_list_name == current_shopping_list:
self.speak("The shopping list %s already exists" % self.new_shopping_list_name )
break
else:
self.speak("I found a similar naming list called %s" % current_shopping_list)
# This hands off to either handle_dont_create_anyways_context or handle_create_anyways_context
# to make a context aware decision
self.speak("Would you like me to add your new list anyways?", expect_response=True)
break
else:
self.speak("Ok creating a new list called %s" % self.new_shopping_list_name)
except AttributeError:
pass
# This is not called directly, but instead should be triggered
# as part of context aware decisions
@intent_handler(IntentBuilder('DoNotAddIntent').require("NoKeyword").require('CreateAnywaysContext').build())
@removes_context("CreateAnywayscontext")
def handle_dont_create_anyways_context(self):
"""
Does nothing but acknowledges the user does not wish to proceed
Uses dont.add.response.dialog
:return:
"""
self.speak_dialog('dont.add.response')
# This function is also part of the context aware decision tree
@intent_handler(IntentBuilder('AddAnywaysIntent').require("YesKeyword").require('CreateAnywaysContext').build())
@removes_context("CreateAnywayscontext")
def handle_create_anyways_context(self):
"""
If the user wants to create a similarly named list, it is handled here
Uses do.add.response.dialog
:return:
"""
self.speak_dialog('do.add.response')
def stop(self):
pass
def create_skill():
return OurGroceriesSkill()
在下一篇文章中,我将介绍日志记录、从 Web UI 获取设置,并继续完善技能,使其更有用。
3 条评论