由于最近的全球疫情和居家令,我一直在寻找可以替代我通常活动的事情。我开始更新我的家庭电子设备设置,并以此为契机深入研究家庭自动化。我的一些朋友使用亚马逊的 Alexa 来打开和关闭他们家里的灯,这在某种程度上很吸引人。但是,我是一个注重隐私的人,我从来没有真正习惯谷歌或亚马逊的设备一直监听我的家人(为了这次谈话,我会忽略手机)。我大约四年前就知道了开源语音助手 Mycroft,但由于早期项目的挣扎,我从未对其进行过深入调查。自从我第一次偶然发现该项目以来,该项目已经取得了长足的进步,它为我检查了很多方面
- 自托管
- 易于上手(通过 Python)
- 开源
- 注重隐私
- 互动聊天频道
在本系列的第一篇文章中,我介绍了 Mycroft,在第二篇文章中,我谈到了人工智能技能的概念。在其最基本的形式中,技能是一段代码块,执行该代码块以实现意图所需的結果。意图试图确定你想要什么,而技能是 Mycroft 的回应方式。如果你能想到一个结果,那么可能有一种方法可以创建一个技能来实现它。
Mycroft 技能的核心只是 Python 程序。一般来说,它们有三个或四个部分
- import 部分是你加载完成任务所需的任何 Python 模块的地方。
- 可选的 function 部分包含在主类部分之外定义的代码片段。
- class 部分是所有魔法发生的地方。类应始终将
MycroftSkill
作为参数。 - create_skill() 部分是 Mycroft 用来加载你的技能的部分。
当我编写技能时,我通常首先编写一个标准的 Python 文件,以确保我的代码执行我所认为的操作。我这样做主要是因为我习惯的工作流程(包括调试工具)存在于 Mycroft 生态系统之外。因此,如果我需要单步调试我的代码,我发现使用我的 IDE (PyCharm) 及其内置工具更加熟悉,但这只是个人偏好。
此项目的所有代码都可以在我的 GitLab 仓库中找到。
关于意图解析器
此项目中的技能同时使用了 Padatious 和 Adapt 意图解析器,我在我之前的文章中描述过它们。为什么?首先,本教程旨在提供一个具体的示例,说明你可能希望在自己的技能中考虑使用的一些功能。其次,Padatious 意图非常简单,但不支持正则表达式,而 Adapt 则很好地利用了正则表达式。此外,Padatious 意图不是上下文感知的,这意味着,虽然你可以提示用户做出响应,然后按照一些决策树矩阵解析它,但你最好使用带有 Mycroft 内置上下文处理程序的 Adapt 意图解析器。请注意,默认情况下,Mycroft 假定你正在使用 Padatious 意图处理程序。最后,值得注意的是,Adapt 是一个关键字意图解析器。如果你不是正则表达式忍者,这可能会使复杂的解析变得笨拙。(我不是。)
实施 3T 原则
在你开始编写技能之前,请考虑 3T 原则:认真思考!类似于你为文章写提纲时,当你开始开发技能时,写下你希望你的技能做什么。
本教程将逐步介绍如何编写 Mycroft 技能以将项目添加到 OurGroceries 应用程序(我与该应用程序无关)。事实上,这个技能是我妻子的主意。她想要一个可以在手机上使用的应用程序来管理她的购物清单。我们尝试了近十几个应用程序,试图满足我们各自的需求——我需要一个 API 或一种可以轻松与后端交互的方式,她有一长串标准,其中最重要的是它易于从她的手机上使用。在她列出必备品、锦上添花和愿望清单项目之后,我们选择了 OurGroceries。它没有 API,但它确实有一种通过 JSON 与之交互的方式。甚至有一个名为 py-our-groceries
的方便的库在 PyPI 中(我为它贡献了一些少量内容)。
一旦我有了目标和目标平台,我就开始概述该技能需要做什么
- 登录/身份验证
- 获取当前购物清单的列表
- 将项目添加到特定的购物清单
- 将项目添加到特定列表下的类别
- 添加类别(因为 OurGroceries 允许将项目放入类别中)
考虑到这一点,我开始草拟所需的 Python。这就是我想出的。
创建 Python 草图
通过阅读 py-our-groceries
库的示例,我弄清楚我只需要导入两件事:asyncio
和 ourgroceries
。
很简单。接下来,我知道我需要使用 username
和 password
进行身份验证,并且我知道程序需要执行哪些任务。所以我的草图最终看起来像这样
import asyncio
from ourgroceries import OurGroceries
import datetime
import json
import os
USERNAME = ""
PASSWORD = ""
OG = OurGroceries(USERNAME, PASSWORD)
def fetch_list_and_categories():
pass
def return_category_id():
pass
def add_to_my_list():
pass
def add_category():
pass
我不会详细介绍这个草图的运行原理,因为这超出了本系列的范围。但是,如果你愿意,你可以查看完整的工作大纲。
在你开始编程之前,你需要拥有你的用户名、密码和列表 ID。用户名和密码是显而易见的。列表 ID 可以从单击链接后的 URL 中检索,或者更编程地,你可以使用你选择的浏览器的开发者工具并检查对象。这是开发者工具在 Firefox 中的样子

一旦你有了列表 ID,登录 OurGroceries 并获取 cookie。为此,创建一个 OurGroceries 对象,然后将其传递给 asyncio
。当你在做这件事时,你不妨定义你的列表 ID,也一样
OG = OurGroceries(USERNAME, PASSWORD)
asyncio.run(OG.login())
MY_LIST_ID = "a1kD7kvcMPnzr9del8XMFc"
对于此项目的目的,你需要定义两种对象类型来帮助组织你的代码:groceries
和 categories
。fetch_list_and_categories
方法非常简单
def fetch_list_and_categories(object_type=None):
if object_type == "groceries":
list_to_return = asyncio.run(OG.get_list_items(list_id=MY_LIST_ID))
elif object_type == "categories":
list_to_return = asyncio.run(OG.get_category_items())
else:
list_to_return = None
return (list_to_return)
OurGroceries 允许你添加多个具有相同名称的类别或项目。例如,如果你的列表中已经有“Meat”,并且你再次添加它,你将看到一个名为“Meat (2)”的类别(每当你创建一个具有相同名称的类别时,此数字都会递增)。对我们来说,这是不希望的行为。我们也希望尽可能避免重复,所以我对检测复数做了初步尝试;例如,我的代码同时检查“Meat”和“Meats”。我确信有一种更智能的方式来执行这些检查,但此示例突出了你在进步过程中可能需要考虑的一些事项。为了简洁起见,我将省略这些检查,因此 return_category_id
方法看起来像这样
def return_category_id(category_to_search_for, all_categories):
category_to_search_for_lower = category_to_search_for.lower()
category_id = None
if len(all_categories['list']['items']) is not 0:
for category_heading in all_categories['list']['items']:
# Split the heading because if there is already a duplicate it
# presents as "{{item}} (2)"
category_heading_lowered = category_heading['value'].lower().split()[0]
if category_to_search_for_lower == category_heading_lowered:
category_id = category_heading['id']
break
return(category_id)
要将项目添加到列表,你想要
- 检查该项目是否已存在
- 获取类别 ID
- 将项目添加到特定类别下的列表(如果指定)
add_to_my_list
方法最终看起来像这样
def add_to_my_list(full_list, item_name, all_categories, category="uncategorized"):
# check to make sure the object doesn't exist
# The groceries live in my_full_list['list']['items']
# Start with the assumption that the food does not exist
food_exists = False
toggle_crossed_off = False
category_lowered = category.lower()
for food_item in full_list['list']['items']:
if item_name in food_item['value']:
print("Already exists")
food_exists = True
if not food_exists:
category_id = return_category_id(category_lowered, all_categories)
asyncio.run(OG.add_item_to_list(MY_LIST_ID, item_name, category_id))
print("Added item")
最后,add_category
运行 asyncio
命令以在类别尚不存在时创建类别
def add_category(category_name, all_categories):
category_id = return_category_id(category_name, all_categories)
if category_id is None:
asyncio.run(OG.create_category(category_name))
refresh_lists()
print("Added Category")
else:
print("Category already exists")
你现在应该能够测试你的草图,以确保每个函数中的所有内容都能正常工作。一旦你对草图感到满意,你就可以继续考虑如何在 Mycroft 技能中实现它。
计划 Mycroft 技能
你可以应用与草拟 Python 相同的原则来开发 Mycroft 技能。官方文档建议使用名为 Mycroft Skills Kit 的交互式辅助程序来设置技能。mycroft-msk create
会要求你
- 命名你的技能
- 输入一些常用的短语来触发你的技能
- 确定 Mycroft 应该用什么对话来回应
- 创建技能描述
- 从
fontawesome.com/cheatsheet
中选择一个图标 - 从
mycroft.ai/colors
或color-hex.com
中选择一种颜色 - 定义技能所属的类别(或多个类别)
- 指定代码的许可证
- 说明该技能是否具有依赖项
- 指示你是否要创建 GitHub 仓库
这是 mycroft-msk create
如何工作的一个演示

(Steve Ovens,CC BY-SA 4.0)
在你回答完这些问题后,Mycroft 会在 mycroft-core/skills/<skill name>
下创建以下结构
├── __init__.py
├── locale
│ └── en-us
│ ├── ourgroceries.dialog
│ └── ourgroceries.intent
├── __pycache__
│ └── __init__.cpython-35.pyc
├── README.md
├── settings.json
└── settingsmeta.yaml
你现在可以忽略大多数这些文件。我更喜欢在尝试进行 Mycroft 特有的故障排除之前,确保我的代码正在工作。这样,如果以后出现问题,你就知道这与你的 Mycroft 技能的构建方式有关,而不是代码本身。与 Python 草图一样,查看 Mycroft 在 __init__.py
中创建的大纲。
所有 Mycroft 技能都应该有一个 __init__.py
。按照惯例,所有代码都应该放在这个文件中,尽管如果你是一位熟练的 Python 开发人员并且知道这个文件是如何工作的,你可以选择分解你的代码。
from mycroft import MycroftSkill, intent_file_handler
class OurGroceries(MycroftSkill):
def __init__(self):
MycroftSkill.__init__(self)
@intent_file_handler('ourgroceries.intent')
def handle_test(self, message):
self.speak_dialog('ourgroceries')
def create_skill():
return OurGroceries()
理论上,此代码将基于你在 msk create
过程中创建的触发器执行。Mycroft 首先尝试查找扩展名为 .dialog
的文件,该文件与传递给 selfspeak_dialog()
的参数匹配。在上面的示例中,Mycroft 将查找名为 ourgroceries.dialog
的文件,然后说出它在那里找到的短语之一。如果找不到,它将说出文件名。我将在关于响应的后续文章中对此进行更详细的介绍。如果你想尝试这个过程,请随意探索你可以在技能创建过程中想出的各种输入和输出短语。
虽然该脚本是一个很好的起点,但我更喜欢自己思考 __init__.py
。如前所述,此技能将同时使用 Adapt 和 Padatious 意图处理程序,并且我还想演示会话上下文处理(我将在下一篇文章中更深入地探讨)。所以首先导入它们
from mycroft import intent_file_handler, MycroftSkill, intent_handler
from mycroft.skills.context import adds_context, removes_context
如果你想知道,你在 Python 中指定导入语句的顺序无关紧要。导入完成后,查看类结构。如果你想了解更多关于类及其用途的信息,Real Python 有一个关于该主题的精彩入门读物。
与上面一样,首先用其预期功能模拟你的代码。本节使用与 Python 草图相同的目标,因此继续插入一些内容,这次添加一些注释以帮助指导你
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
__init__
和 initialize
方法
技能有一些你应该了解的“特殊”函数。__init__(self)
方法在首次实例化技能时调用。在 Python IDE 中,在 __init__
部分之外声明的变量通常会导致警告。因此,它们通常用于声明变量或执行设置操作。但是,虽然你可以声明旨在匹配技能设置文件的变量(稍后会详细介绍),但你不能使用 Mycroft 方法(例如 self.settings.get)
来检索值。通常不适合尝试从 __init__
建立与外部世界的连接。此外,__init__
函数在 Mycroft 中被认为是可选的。大多数技能选择拥有一个,并且它被认为是“Pythonic”的做事方式。
initialize
方法在技能完全构建并向系统注册后调用。它用于执行技能的任何最终设置,包括访问技能设置。但是,它是可选的,我选择创建一个函数来获取身份验证信息。如果你好奇并想提前查看,我将其称为 _create_initial_grocery_connection
。当我开始逐步创建技能代码时,我将在下一篇文章中重新讨论这两个特殊函数。
最后,有一个名为 stop()
的特殊函数,我没有使用它。每当用户说“stop”时,都会调用 stop 方法。如果你有一个长时间运行的进程或音频播放,此方法非常有用。
总结
所以你现在有了你想要完成的任务的大纲。随着时间的推移,这肯定会增长。在你开发技能时,你会发现你的技能需要新的功能才能最佳地工作。
下次,我将讨论你将使用的意图类型、如何设置它们以及如何处理正则表达式。我还将探讨会话上下文的想法,会话上下文用于从用户那里获取反馈。
你有什么意见、问题或疑虑吗?发表评论,在 Twitter 上访问我 @linuxovens,或访问 Mycroft 技能聊天频道。
评论已关闭。