在本系列关于 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
创建的(在第三篇文章中)有一个看起来很奇怪的函数 @intent_file_handler('ourgroceries.intent')
,在 handle_test
方法之上。 这些是 Python 中的特殊符号,称为装饰器(如果需要,请前往 Real Python 了解Python 装饰器入门)。 对于本教程,只需知道装饰器是将您的函数传递到 Mycroft 开发的预构建函数中的一种方式。 这节省了大量工作和样板代码。
回想一下本系列的第三部分,该项目使用了两个意图解析器:Padacious 和 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 误解用户声音的问题。 从上面的列表中,哪些信息在编程上相关? 西红柿
、购物清单
、购物清单
和 Costco 清单
。 官方文档将此类对象称为*实体*。 如果您觉得这样更容易理解,您可以将实体视为变量。 当您创建意图文件时,这将变得更加清晰。 虽然 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
的一部分。 因为该语句中包含单词 my
,所以与该语句匹配的句子可能是
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 匹配的每个短语,只需在意图文件中添加一行即可。 但是,对于复杂的意图,您可能需要几十行来尝试涵盖您想要处理的所有不同语句。
还有另一种值得考虑的选项。 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 skills 子系统后)。如果您愿意,您可以更进一步将其压缩成一行。
add {Food} to ( | my | the ) {ShoppingList} ( | under {Category})
注意: 为了更容易阅读,请在您的意图圆括号扩展中使用空格。
总结关于 Padatious 意图解析的要点:
- 您必须提供几个短语示例,Mycroft 才能提出正确的匹配。
- Padatious 意图使用诸如
{Food}
之类的实体来识别可以从您的 Python 代码中检索的对象值。 - 实体始终是小写,无论您在意图文件中如何声明它们。
- 如果无法从发出的语句中解析实体,则其值为
None
。 - Padatious 意图的装饰器是
@intent_file_handler('my.intent.file.intent')
。
Adapt 意图
与 Padatious 意图不同,在 Padatious 意图中您需要在意图文件中指定实体,而 Adapt 意图解析器则使用一系列关键词,这些关键词与正则表达式 (regex) 文件结合使用,以尝试捕获实体。在以下情况下,您会使用 Adapt 而不是 Padatious:
- 预期发出的语句很复杂,并且需要更强大的正则表达式解析。
- 希望或需要 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 skills 子系统并尝试一下。您应该在 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 条评论