使用意图解析器进行开源家庭自动化项目

使用 Padatious、Adapt、对话上下文和对话框开始编写您的第一个语音 AI 程序。
132 位读者喜欢这篇文章。
Working from home at a laptop

Opensource.com

在本系列关于 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.intentcreate.category.intent,则不会混淆您的变量是从哪个文件填充的,因为您必须按文件名调用它们。正如您在 mycroft-msk 的输出中看到的那样,装饰器直观地命名为 @intent_file_handler()。只需使用文件名作为装饰器的参数,例如 @intent_file_handler("create.item.intent")

想想人们可能会用哪些短语将商品添加到购物清单。由于此技能的动机是使用 Mycroft 创建购物清单,因此示例代码使用了与食物相关的术语,但您可以使用通用术语。话虽如此,以下是一些您可能会说出的将商品添加到购物清单的短语

  • 将西红柿添加到我的购物清单
  • 将西红柿添加到购物清单
  • 将西红柿添加到 Costco 清单

您也可以选择使用一些语法不正确的短语,以解释 Mycroft 对用户声音的误解。从上面的列表中,哪些信息在编程上是相关的?tomatoesshopping listgrocery listCostco list。官方文档将这种类型的对象称为实体。如果您觉得这样更容易理解,您可以将实体视为变量。当您创建意图文件时,这将变得更加清晰。虽然 mycroft-msk 命令默认会将意图放在 locale/en-us 下,但我将我的意图放在 vocab/en-us/ 下。为什么?嗯,那是因为 Adapt 意图解析器将其文件存储在 vocab 中,我更喜欢将所有意图文件保存在同一位置。我的文件 vocab/en-us/create.item.intent

add {Food} to my {ShoppingList}

这定义了实体 FoodShoppingList

重要提示: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 将无法弄清楚其中一个短语背后的意图。您能猜出是哪个以及为什么吗?

如果视频速度有点快,这里是答案: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 部分中此技能的目标

  1. 登录/身份验证
  2. 获取当前购物清单的列表
  3. 将商品添加到特定购物清单
  4. 将商品添加到特定清单下的类别
  5. 能够添加类别(因为 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))

这是一个测试这些代码更改的示例

在示例中,Mycroft 回应 >> Adding nails to my hardware list under,但您告诉 Mycroft 说单词 under 的唯一时间是当 category_name 的值不是 None 时。这是因为意图解析器将单词 under 解释为实体 ShoppingList 的一部分。因为 utterance 中有单词 my,所以与 utterance 匹配的句子可能是

  1. add {Food} to my {ShoppingList}

  2. 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 语句重写此内容以组合 mythe 关键字,您可以编写

add {Food} to ( my | the ) {ShoppingList}
add {Food} to ( my | the ) {ShoppingList} under {Category}
add {Food} to {ShoppingList}
add {Food} to {ShoppingList} under {Category}

这从意图中删除了两行。括号扩展还支持使某些内容成为可选的。因此,如果您想使 themy 成为可选的,从而允许短语 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

  1. 期望 utterance 是复杂的,并且需要更强大的 regex 解析
  2. 希望或需要 Mycroft 具有上下文感知能力
  3. 需要意图尽可能轻量级

也就是说,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 默认技能使用 startcreate,我需要添加单词,以便 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.vocNoKeyword.voc,并将单词 yesno 分别放在其中。

现在在您的 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')

这里有两件事您到目前为止还没有见过

  1. @remove_context
  2. 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 获取设置,并继续完善技能,使其更有用。

接下来阅读什么
User profile image.
Steve 是一位敬业的 IT 专业人士和 Linux 倡导者。在加入 Red Hat 之前,他在金融、汽车和电影行业工作了几年。Steve 目前在 Red Hat 担任解决方案和技术实践架构师。他拥有从 RHCA(DevOps 领域)到 Ansible,再到容器化应用程序等方面的认证。

3 条评论

即使是笨蛋也能理解,好文章

如果您可以发布一篇关于用于缩小图片尺寸的解析器的文章,那就太好了。

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.