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

使用 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 创建的(在第三篇文章中)有一个看起来很奇怪的函数 @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.intentcreate.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}

这定义了实体 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 的一部分。 因为该语句中包含单词 my,所以与该语句匹配的句子可能是

  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 匹配的每个短语,只需在意图文件中添加一行即可。 但是,对于复杂的意图,您可能需要几十行来尝试涵盖您想要处理的所有不同语句。

还有另一种值得考虑的选项。 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 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:

  1. 预期发出的语句很复杂,并且需要更强大的正则表达式解析。
  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 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.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 条评论

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

如果您能发表一篇关于用于缩小图片的解析器的文章,那就太好了。

我不确定我是否理解,你能否详细说明你的想法?

回复 ,作者:Jeremaia34

Creative Commons License该作品已获得 Creative Commons Attribution-Share Alike 4.0 International License 的许可。
© . All rights reserved.