本文共 25281 字,大约阅读时间需要 84 分钟。
开源的词法分析器
在开源语音助手的本系列的和 ,我为学习如何创建技能奠定了基础。 在 ,我逐步创建了技能概述,并建议首先在纯Python中创建技能,以确保方法按预期工作。 这样,当出现问题时,您就知道这与Mycroft技能的构建方式有关,而与代码本身无关。
在本文中,您将通过添加以下内容来增强第3部分中的概述:
该项目的代码可以在存储库中找到。
让我们开始吧!
提醒一下,该项目的目的是使用Mycroft将商品添加到应用程序中的购物清单中。 但是,本教程可以应用于各种家庭自动化应用程序,例如打开灯,获取早晨天气报告或控制娱乐系统。
到目前为止,此技能的轮廓如下所示:
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,以获取的 )。 对于本教程,足以知道装饰器是将函数传递到Mycroft开发的预构建函数的一种方式。 这样可以节省大量工作和样板代码。
回想一下本系列的第三部分,该项目使用了两个意图解析器:Padacious和Adapt,我在第二篇文章中对此进行了描述。
Mycroft将用来训练意图的短语放置在带有.intent
文件扩展名的文件中。 您可以有多个.intent
文件,但是必须显式引用每个文件。 这意味着,如果您具有create.item.intent
和create.category.intent
,则不会混淆变量是从哪个文件填充的,因为您必须按文件名进行调用。 从mycroft-msk
的输出中可以看到,装饰器直观地命名为@intent_file_handler()
。 只需使用文件名作为装饰器的参数,例如@intent_file_handler("create.item.intent")
。
考虑一下某人可能用来将商品添加到购物清单的短语。 由于此技能的推动因素是使用Mycroft创建购物清单,因此示例代码使用与食品相关的术语,但您可以使用通用术语。 如此说来,您可能会说以下几句话,以便将商品添加到购物清单:
您也可以选择使用一些语法错误的短语,以解决Mycroft对用户语音的误解。 从上面的列表中,哪些信息与程序相关? tomatoes
, shopping list
, grocery list
和Costco list
。 官方文档将此类对象称为实体 。 您可以将实体视为变量,如果这对您更有意义。 创建意向文件后,这一点将变得更加清晰。 虽然mycroft-msk
命令将默认情况下将意图放在locale/en-us
,但我将我的意图放在vocab/en-us/
。 为什么? 好吧,这是因为Adapt意图分析器将其文件存储在vocab
,而我更喜欢将所有意图文件都保留在同一位置。 我的文件vocab/en-us/create.item.intent
以以下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将无法弄清楚这些短语之一背后的意图。 你能猜出是哪一个,为什么?
如果视频对您来说太快了,那就是答案:Mycroft无法处理add buns to groceries list
的短语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
做出的任何响应都以>>
mycroft-cli-client
,以指示其响应。 现在您已经有了基本的意图,请返回并回顾本系列第3部分中的此技能的目标:
现在先忽略前两个项目,它们是项目的在线部分,您需要首先完成其他目标。 对于第三项,从理论上讲,您的基本意图是应该能够采用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
,所以与话语匹配的句子可能是:
add {Food} to my {ShoppingList}
要么 add {Food} to my {ShoppingList} under {Category}
由于用户未声明{Category}
,因此Mycroft选择了最正确的第一个语句。 这意味着在单词my
之后的所有内容都将被强制转换为实体{ShoppingList}
。 因此,由于{Category}
为None
,Mycroft会说“将钉子添加到我的硬件列表中”而不是“将钉子添加到我的硬件列表中”。
首先,Patatious似乎有点简单。 对于您需要Mycroft匹配的每个短语,只需在意图文件中添加一行即可。 但是,出于复杂的目的,您可能有几十行试图覆盖您要处理的所有不同话语。
还有另一种选择可能值得考虑。 贪心的意图支持括号的扩大 。 这意味着您可以使用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意向分析的要点:
{Food}
实体来标识可以从您的Python代码中检索到的对象值。 None
。 @intent_file_handler('my.intent.file.intent')
。 与Padatious意图不同,后者在意图文件中指定实体,而Adapt意图解析器则使用一系列与正则表达式(regex)文件结合使用的关键字来尝试捕获实体。 在以下情况下,您将使用Adapt over Padatious:
就是说,适应使用的voc
文件非常灵活。 它们可以包含一个单词(如所示),也可以包含您要响应的句子的开头。
该项目的一个目标是让Mycroft在OurGroceries应用程序中创建一个新的购物清单,我想添加一些基本检查,以便通知用户是否存在名称相似的清单并询问他们是否仍要创建一个新列表。 这应该减少列表重复和项目的错位。
模拟一些代码,然后即可处理vocab和regex文件。 尽管可以使用Pytest或类似的单元测试来声明特定的值,但为简单起见,您将创建一个称为“购物清单”的清单。 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提示用户进行响应并存储结果。 我将在会话上下文部分中进一步讨论这段代码。
现在您有了一些伪代码,但是您需要添加Mycroft的装饰器来操作您的代码。 为此,您需要创建三个文件:两个vocab文件和一个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
。 为了检查语法,我建议使用 。 当我调试我的正则表达式时,这非常有帮助(我在正则表达式上非常糟糕)。
重要说明:适应意图区分大小写。
现在,您的正则表达式包含了您期望的完整句子,现在创建两个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
实际上是来自正则表达式文件的实体,则require
的参数是您在正则表达式中定义的实体的名称。 如果您的正则表达式正在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理解他是指John Quincy Adams。
回到创建会话上下文时,其装饰器的参数是上下文的名称。 此示例调用上下文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标准。 可以在的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获取设置,并继续将这项技能填充到更有用的内容中。
翻译自:
开源的词法分析器
转载地址:http://vjszd.baihongyu.com/