如何准备使用 Python 编写你的第一个 Mycroft AI 技能

计划是编写技能和教会 Mycroft 如何执行你想要它执行的操作的必要第一步。
119 位读者喜欢这个。
How to turn a Raspberry Pi into an eBook server

Opensource.com

由于最近的全球疫情和居家令,我一直在寻找可以替代我通常活动的事情。我开始更新我的家庭电子设备设置,并以此为契机深入研究家庭自动化。我的一些朋友使用亚马逊的 Alexa 来打开和关闭他们家里的灯,这在某种程度上很吸引人。但是,我是一个注重隐私的人,我从来没有真正习惯谷歌或亚马逊的设备一直监听我的家人(为了这次谈话,我会忽略手机)。我大约四年前就知道了开源语音助手 Mycroft,但由于早期项目的挣扎,我从未对其进行过深入调查。自从我第一次偶然发现该项目以来,该项目已经取得了长足的进步,它为我检查了很多方面

  • 自托管
  • 易于上手(通过 Python)
  • 开源
  • 注重隐私
  • 互动聊天频道

在本系列的第一篇文章中,我介绍了 Mycroft,在第二篇文章中,我谈到了人工智能技能的概念。在其最基本的形式中,技能是一段代码块,执行该代码块以实现意图所需的結果。意图试图确定你想要什么,而技能是 Mycroft 的回应方式。如果你能想到一个结果,那么可能有一种方法可以创建一个技能来实现它。

Mycroft 技能的核心只是 Python 程序。一般来说,它们有三个或四个部分

  1. import 部分是你加载完成任务所需的任何 Python 模块的地方。
  2. 可选的 function 部分包含在主类部分之外定义的代码片段。
  3. class 部分是所有魔法发生的地方。类应始终将 MycroftSkill 作为参数。
  4. create_skill() 部分是 Mycroft 用来加载你的技能的部分。

当我编写技能时,我通常首先编写一个标准的 Python 文件,以确保我的代码执行我所认为的操作。我这样做主要是因为我习惯的工作流程(包括调试工具)存在于 Mycroft 生态系统之外。因此,如果我需要单步调试我的代码,我发现使用我的 IDE (PyCharm) 及其内置工具更加熟悉,但这只是个人偏好。

此项目的所有代码都可以在我的 GitLab 仓库中找到。

关于意图解析器

此项目中的技能同时使用了 PadatiousAdapt 意图解析器,我在我之前的文章中描述过它们。为什么?首先,本教程旨在提供一个具体的示例,说明你可能希望在自己的技能中考虑使用的一些功能。其次,Padatious 意图非常简单,但不支持正则表达式,而 Adapt 则很好地利用了正则表达式。此外,Padatious 意图不是上下文感知的,这意味着,虽然你可以提示用户做出响应,然后按照一些决策树矩阵解析它,但你最好使用带有 Mycroft 内置上下文处理程序的 Adapt 意图解析器。请注意,默认情况下,Mycroft 假定你正在使用 Padatious 意图处理程序。最后,值得注意的是,Adapt 是一个关键字意图解析器。如果你不是正则表达式忍者,这可能会使复杂的解析变得笨拙。(我不是。)

实施 3T 原则

在你开始编写技能之前,请考虑 3T 原则:认真思考!类似于你为文章写提纲时,当你开始开发技能时,写下你希望你的技能做什么。

本教程将逐步介绍如何编写 Mycroft 技能以将项目添加到 OurGroceries 应用程序(我与该应用程序无关)。事实上,这个技能是我妻子的主意。她想要一个可以在手机上使用的应用程序来管理她的购物清单。我们尝试了近十几个应用程序,试图满足我们各自的需求——我需要一个 API 或一种可以轻松与后端交互的方式,她有一长串标准,其中最重要的是它易于从她的手机上使用。在她列出必备品、锦上添花和愿望清单项目之后,我们选择了 OurGroceries。它没有 API,但它确实有一种通过 JSON 与之交互的方式。甚至有一个名为 py-our-groceries方便的库在 PyPI 中(我为它贡献了一些少量内容)。

一旦我有了目标和目标平台,我就开始概述该技能需要做什么

  1. 登录/身份验证
  2. 获取当前购物清单的列表
  3. 将项目添加到特定的购物清单
  4. 将项目添加到特定列表下的类别
  5. 添加类别(因为 OurGroceries 允许将项目放入类别中)

考虑到这一点,我开始草拟所需的 Python。这就是我想出的。

创建 Python 草图

通过阅读 py-our-groceries 库的示例,我弄清楚我只需要导入两件事:asyncioourgroceries

很简单。接下来,我知道我需要使用 usernamepassword 进行身份验证,并且我知道程序需要执行哪些任务。所以我的草图最终看起来像这样

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 中的样子

Getting an ID
CC BY-SA Steve Ovens

一旦你有了列表 ID,登录 OurGroceries 并获取 cookie。为此,创建一个 OurGroceries 对象,然后将其传递给 asyncio。当你在做这件事时,你不妨定义你的列表 ID,也一样

OG = OurGroceries(USERNAME, PASSWORD)
asyncio.run(OG.login())
MY_LIST_ID = "a1kD7kvcMPnzr9del8XMFc"

对于此项目的目的,你需要定义两种对象类型来帮助组织你的代码:groceriescategoriesfetch_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)

要将项目添加到列表,你想要

  1. 检查该项目是否已存在
  2. 获取类别 ID
  3. 将项目添加到特定类别下的列表(如果指定)

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/colorscolor-hex.com 中选择一种颜色
  • 定义技能所属的类别(或多个类别)
  • 指定代码的许可证
  • 说明该技能是否具有依赖项
  • 指示你是否要创建 GitHub 仓库

这是 mycroft-msk create 如何工作的一个演示

在你回答完这些问题后,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 开发人员并且知道这个文件是如何工作的,你可以选择分解你的代码。

在 Mycroft 创建的文件中,你可以看到

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 技能聊天频道

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

评论已关闭。

© . All rights reserved.