65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Oscova 的数据库机器人

starIconstarIconstarIconstarIconstarIcon

5.00/5 (16投票s)

2017年1月17日

CPOL

16分钟阅读

viewsIcon

37575

downloadIcon

776

使用 Oscova(一个机器人开发框架)为 SQL 数据库创建自然语言接口

引言

很久以前,我开始使用 SIML 来为数据库创建自然语言接口。收到的反馈让我应接不暇。所以这次,我决定重拾旧业,并用一个全新的架构 Oscova 来更进一步。Oscova 是 Syn Bot 开发框架的一个新成员。

Oscova 与 SIML 的不同之处在于,与 SIML 不同,Oscova 不依赖于严格的模式。相反,它依赖于用户输入和保存的表达式之间的语义和句法相似性。Oscova 使开发人员能够创建利用对话 (Dialogs)、意图 (Intents)、上下文 (Contexts) 和命名实体识别 (Named Entity Recognition) 等概念和功能的机器人,并且无需连接任何在线 API。

说了这么多,如果您仍然偏好编写严格的用户输入模式脚本,SIML 可能仍然更适合您。

在本文中,我们将利用 Oscova 创建一个允许我们与 `employees` 数据库进行交互的机器人。

必备组件

  • C# 编程知识
  • SQL 数据库管理
  • WPF 应用程序和 XAML 的基本概念

架构

本质上,Oscova 的构建块主要包括以下内容:

  • Dialog - 仅仅是相关 `intents` 的集合
  • Intent - 当 `Expression` 匹配时,机器人执行/调用的操作
  • Entity - 识别的用户或系统实体
  • Expression - 类似于用户输入的模板模式或示例

涉及的步骤

  • 首先,我们将创建一个 WPF 应用程序项目,包含所有必需的 GUI 元素。
  • 将必要的库导入到我们的项目中。
  • 创建一个数据库实用类,用于与我们的数据库进行交互。
  • 将 GUI 与机器人事件连接起来。
  • 为几个数据库列创建实体识别器。
  • 创建对话框 (Dialogs) 和意图 (Intents)
  • 测试我们的机器人。

如果您感到困惑或发现任何难以理解的概念,只需打开项目并参考代码。一旦您浏览了项目的代码库,事情就会变得容易得多。

WPF 美化

为了简单起见,我本可以使用控制台应用程序,但我喜欢锦上添花,所以让我们首先创建一个 WPF 项目,它将包含与机器人交互所需的所有 GUI 内容。

在本文中,我将假定您使用的是 Visual Studio。如果您在 Linux 环境下,Mono with GTK 应该足够了,但本文我将坚持使用 VS。

  • 启动 Visual Studio。
  • 点击 **文件**,选择 **新建**,然后选择 **项目…**
  • 在 **模板** 下,选择 **Windows**,然后选择 **WPF Application**。
  • 将项目命名为 `NLI-Database-Oscova`(如果您计划复制代码)。NLI 是 `Natural Language Interface`(自然语言接口)的首字母缩写。

WPF 应用程序需要几个组件:

  • TextBox - 用于用户输入
  • Button - 用于评估用户输入
  • TabControl - 第一个选项卡用于查看 `DataGrid`(用于数据库),第二个选项卡用于查看 JSON 结果

在解决方案资源管理器中,双击 `MainWindow.xaml` 并用下面的代码替换 XAML。

<c1></c1><Window x:Class="NLI_Database_Oscova.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Natural Language Interface using Oscova" Height="450" 
        Width="560" WindowStartupLocation="CenterScreen">
    <Grid>
        <TabControl>
            <TabItem Header="Interaction">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="35"/>
                        <RowDefinition Height="50*"/>
                        <RowDefinition Height="232*"/>
                    </Grid.RowDefinitions>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="414*"/>
                            <ColumnDefinition Width="100"/>
                        </Grid.ColumnDefinitions>
                        <TextBox Name="InputBox" TextAlignment="Center" 
                         CharacterCasing="Upper" KeyDown="InputBox_OnKeyDown"/>
                        <Button Name="ExecuteButton" Content="Evaluate" 
                         Grid.Column="1" Click="EvaluateButton_OnClick"/>
                    </Grid>
                    <Label Grid.Row="1"  Name="ResponseLabel" 
                     Content="No Response Yet" VerticalContentAlignment="Top" />
                    <TabControl Grid.Row="2">
                        <TabItem Header="Table">
                            <DataGrid  Name="EmployeeGrid" FontSize="14" />
                        </TabItem>
                        <TabItem Header="JSON Result">
                            <TextBox Name="ResultBox" IsReadOnly="True" 
                             AcceptsReturn="True" VerticalScrollBarVisibility="Auto" />
                        </TabItem>
                    </TabControl>
                </Grid>
            </TabItem>
            <TabItem Header="Examples">
                <TextBox Name="ExamplesBox" VerticalScrollBarVisibility="Auto" 
                 HorizontalScrollBarVisibility="Auto" IsReadOnly="True"/>
            </TabItem>
        </TabControl>
    </Grid>
</Window>

上面的代码添加了一个 `TabControl`,其中包含用于用户输入的 `TextBox` 和一个用于评估的 `Button`。

WPF 应用程序看起来应该像这样:

注意:忽略 XAML 代码中的任何 *异常*。我们将在本文稍后处理它们。

导入 Oscova 和 SQLite

现在您的 WPF 应用程序已经有了些许“妆容”,让我们来引用 Oscova。在此之前,请允许我澄清一下,Oscova 是 `Syn.Bot` 框架的一部分,我们需要使用 NuGet 来引用该包。

  • 点击 **工具**,选择 **NuGet 包管理器**,然后选择 **程序包管理器控制台**。
  • 输入 `Install-Package Syn.Bot`
  • Syn.Bot 框架将与一些外部依赖项一起被引用。

引用 *SQLite*(用于数据库)

  • 输入 `Install-Package System.Data.SQLite`

现在您已经成功引用了所需的库。我们将继续动手进行一些 C# 编码。

DatabaseUtility 类

为了让事情更简单,我们首先创建一个数据库实用类。在 **解决方案资源管理器** 中右键单击项目,选择 **类**。将类命名为 `DatabaseUtility`。

用以下内容替换 `DatabaseUtility` 类的全部内容。

using System.Data.SQLite;
using System.IO;

namespace NLI_Database_Oscova
{
    public class DatabaseUtility
    {
        const string DataSource = "EmployeesTable.db";
        public SQLiteCommand Command { get; set; }
        public SQLiteConnection Connection { get; set; }
        public MainWindow Window { get; }

        public DatabaseUtility(MainWindow window)
        {
            Window = window;
        }

        public void Initialize()
        {
            if (File.Exists(DataSource)) File.Delete(DataSource);

            Connection = new SQLiteConnection 
                         { ConnectionString = "Data Source=" + DataSource };
            Connection.Open();
            ExecuteCommand("CREATE TABLE IF NOT EXISTS EMPLOYEES 
                          (ID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 
                          Name VARCHAR(100) NOT NULL, Role VARCHAR(10), 
                          Age INTEGER NOT NULL, Salary INTEGER NOT NULL);");

            ExecuteCommand("INSERT INTO EMPLOYEES VALUES(1, 'Lincoln', 'Manager', 43, 54000)");
            ExecuteCommand("INSERT INTO EMPLOYEES VALUES(2, 'George', 'CEO', 46, 75000)");
            ExecuteCommand("INSERT INTO EMPLOYEES VALUES(3, 'Rick', 'Admin', 32, 18000)");
            ExecuteCommand("INSERT INTO EMPLOYEES VALUES(4, 'Jorge', 'Engineer', 28, 35000)");
            ExecuteCommand("INSERT INTO EMPLOYEES VALUES(5, 'Ivan', 'Tech', 23, 34000)");

            ExecuteCommand("INSERT INTO EMPLOYEES VALUES(6, 'Mark', 'Tech', 25, 34000)");
            ExecuteCommand("INSERT INTO EMPLOYEES VALUES(7, 'Vincent', 'Support', 21, 20000)");
            ExecuteCommand("INSERT INTO EMPLOYEES VALUES(8, 'Wang', 'Support', 20, 20000)");
            ExecuteCommand("INSERT INTO EMPLOYEES VALUES(9, 'Ahmed', 'Tech', 24, 34000)");
            ExecuteCommand("INSERT INTO EMPLOYEES VALUES(10, 'Krishna', 'Admin', 25, 18000)");
        }

        private void ExecuteCommand(string commandText)
        {
            Command = new SQLiteCommand(Connection) { CommandText = commandText };
            Command.ExecuteNonQuery();
        }
    }
}

稍等一下,还有更多要添加的。在 `DatabaseUtility` 类中,添加以下方法:

public void Evaluate(string commandText)
{
    Window.UpdateDataGrid(commandText);
}

public void UpdatePropertyByName
       (string employeeName, string propertyName, string propertyValue)
{
    var updateString = $"UPDATE EMPLOYEES SET {propertyName}={propertyValue} 
                       WHERE UPPER(Name) LIKE UPPER('%{employeeName}%');";
    var selectString = $"SELECT * FROM EMPLOYEES WHERE UPPER(Name) 
                       LIKE UPPER('{employeeName}%');";

    Evaluate(updateString);
    Evaluate(selectString);
}

public void UpdatePropertyById(string employeeId, string propertyName, string propertyValue)
{
    var updateString = $"UPDATE EMPLOYEES SET {propertyName}={propertyValue} 
                       WHERE ID={employeeId};";
    var selectString = $"SELECT * FROM EMPLOYEES WHERE ID={employeeId};";

    Evaluate(updateString);
    Evaluate(selectString);
}

public void PropertyByName(string employeeName, string propertyName)
{
    var selectString = $"SELECT {propertyName} FROM Employees 
                       WHERE UPPER(Name) LIKE UPPER('%{employeeName}%');";
    Evaluate(selectString);
}

public void PropertyByRole(string employeeRole, string propertyName)
{
    var selectString = $"SELECT DISTINCT {propertyName} FROM Employees 
                       WHERE UPPER(Role) LIKE UPPER('%{employeeRole}%')";
    Evaluate(selectString);
}

public void EmployeeByName(string employeeName)
{
    var selectString = $"SELECT * FROM Employees WHERE UPPER(Name) 
                       LIKE UPPER('%{employeeName}%');";
    Evaluate(selectString);
}

public void EmployeeByRole(string employeeRole)
{
    var selectString = $"SELECT * FROM Employees WHERE UPPER(Role) 
                       LIKE UPPER('%{employeeRole}%');";
    Evaluate(selectString);
}

public void Close()
{
    Command.Dispose();
    Connection.Dispose();
}

此类将 `MainWindow` 作为其构造函数参数之一。我们稍后将在项目中使用它。

`Initialize` 方法帮助我们为 `employees` 数据库创建虚拟条目。然后有一些辅助函数,如 `UpdatePropertyByName`、`UpdatePropertyById` 等。这些函数将在调用特定意图时被我们的机器人使用。

目前您不必担心代码。在本文稍后,这一切都会变得有意义。

MainWindow

在 **解决方案资源管理器** 中,右键单击 `MainWindow.cs` 并选择 **查看代码**。

添加以下引用:

using System;
using System.Data;
using System.Data.SQLite;
using System.Windows;
using System.Windows.Input;
using Syn.Bot.Oscova;
using Syn.Bot.Oscova.Collection;
using Syn.Bot.Oscova.Entities;

将以下属性添加到 `MainWindow` 类中。

public OscovaBot Bot { get; }
public DatabaseUtility DatabaseUtility { get; }

在构造函数中添加以下内容:

public MainWindow()
{
    InitializeComponent();

    Bot = new OscovaBot();

    Bot.MainUser.ResponseReceived += (sender, args) =>
    {
        ResponseLabel.Content = args.Response.Text;
    };

    DatabaseUtility = new DatabaseUtility(this);
    DatabaseUtility.Initialize();
    UpdateDataGrid("SELECT * From Employees");

    Bot.MainUser.Context.SharedData.Add(DatabaseUtility);

    Bot.CreateRecognizer("set", new[] { "change", "update", "set" });
    Bot.CreateRecognizer("property", new[] { "id", "name", "job", "age", "salary" });

    //Parsers go here

    //Dialogs go here

    //Finally Train the Bot.
    Bot.Train();
}

那么我们在做什么呢?

首先,我们将 `Bot` 属性赋值为一个实例化的 `OscovaBot` 对象。接下来,我们处理 `MainUser` 的 `ResponseReceived` 事件,通过 `ResponseLabel` 显示响应的文本。

然后,我们创建 `DatabaseUtility` 类的新实例并调用 `Initialize()` 方法。您可以暂时忽略 `UpdateDataGrid()` 方法。

由于我们将在整个聊天会话中需要 `DatabaseUtility`,因此我们将其添加到 `MainUser` 的 `SharedData` 中。

接下来,我们使用重载的 `CreateRecognizer` 方法,为 `change`、`update` 等特定单词创建识别器。

最后,我们调用 `Train()` 方法。

注意:Oscova 要求在添加、创建或修改组件后调用一次 `Train()` 方法。

有了这些,我们继续修复 `UpdateDataGrid()` 函数。将以下代码添加到 `MainWindow` 类中。

public void UpdateDataGrid(string sql)
{
    var dataSet = new DataSet();
    var dataAdapter = new SQLiteDataAdapter(sql, DatabaseUtility.Connection);
    dataAdapter.Fill(dataSet);
    if (dataSet.Tables.Count > 0)
    {
        EmployeeGrid.ItemsSource = dataSet.Tables[0].DefaultView;
    }
}

上面的方法使我们能够在每次 SQL 调用后更新 `DataGrid`。您可能已经注意到 `DatabaseUtility` 使用了这个方法。

创建自定义实体识别器

到目前为止一切顺利。是时候创建一些实体识别器了。实体识别器只是 `IEntityRecognizer` 接口的一个实现,它读取*规范化*的用户输入并返回匹配项的集合。

例如,一个数字识别器可能会使用正则表达式,并将所有匹配的数字作为实体集合返回。

简而言之,我们数据库中的每一项都将被视为一个单独的实体。这允许用户输入包含实体,进而使我们能够创建更好的表达式。

员工姓名识别器

而不是从头开始创建实体识别器,我们将创建一个 `private` 方法,该方法利用 `CreateRecognizer` 重载方法,其第二个参数接受一个返回实体集合的函数,即 `EntityCollection`。

将以下方法添加到 `MainWindow` 类:

private void CreateNameParser()
{
    Bot.CreateRecognizer("Name", request =>
    {
        var requestText = request.NormalizedText;
        var entities = new EntityCollection();

        DatabaseUtility.Command.CommandText = "SELECT * FROM EMPLOYEES";
        var reader = DatabaseUtility.Command.ExecuteReader();

        while (reader.Read())
        {
            var name = reader["Name"].ToString();
            var wordIndex = requestText.IndexOf(name, StringComparison.OrdinalIgnoreCase);
            if (wordIndex != -1)
            {
                var entity = new Entity("Name")
                {
                    Value = name,
                    Index = wordIndex
                };
                entities.Add(entity);
            }
        }

        reader.Close();
        return entities;
    });
}

上面的代码创建了一个新的实体识别器。`Name` 是我们希望用来引用数据库中员工姓名的实体类型名称。

`requestText` 包含用户请求文本的规范化值。规范化值通常是相同的用户输入字符串,但应用了过滤器。更详细地讨论这一点超出了本文的范围。我建议您查阅 API 文档以获得更多解释。

使用 `DatabaseUtility` 类,我们迭代数据库 `name` 列中的所有项。然后我们使用 `IndexOf` 方法来查看值是否存在于用户输入中。如果返回的索引值不是 `-1`,则表示找到了该单词。找到 `name` 时,我们创建一个类型为 `name` 的新实体,并设置 `Value` 和 `Index` 属性。

注意:在 Oscova 中,所有实体都必须指定匹配值首次出现时的起始索引。

员工职位识别器

同样,我们还将为每个 `employee` 的职位创建实体识别器。然而,与上面的实体识别器不同,这里我们只创建一个空的识别器,并用 `Role` 列的值填充 `Entries` 属性。

private void CreateRoleParser()
{
    var parser = Bot.CreateRecognizer("Role");
    DatabaseUtility.Command.CommandText = "SELECT * FROM EMPLOYEES";
    var reader = DatabaseUtility.Command.ExecuteReader();
    while (reader.Read())
    {
        var roleTitle = reader["Role"].ToString();
        if (parser.Entries.Contains(roleTitle)) continue;
        parser.Entries.Add(roleTitle);
    }
    reader.Close();
}

太棒了!现在让我们在 `MainWindow` 构造函数中调用这些方法。将以下代码放在构造函数中 `//Parsers go here` 注释的正下方。

//Parsers go here
CreateNameParser();
CreateRoleParser();

如果您走到这一步,我希望您已经对命名实体识别器有了基本的了解。

处理按钮和文本框事件

为了将用户输入从 `InputBox` 传递给 Oscova 进行评估,我们需要处理 `EvaluateButton.Click` 和 `InputBox.KeyDown` 事件。将以下代码添加到 `MainWindow` 类中。

private void EvaluateButton_OnClick(object sender, RoutedEventArgs e)
{
    var message = string.IsNullOrEmpty(InputBox.Text) ? "clear" : InputBox.Text;
    var result = Bot.Evaluate(message);
    ResultBox.Text = result.Serialize();
    result.Invoke();
    InputBox.Clear();
}

上面的代码检查 `InputBox.Text` 值是否为空。如果为空,则发送 `clear` 命令进行评估。

在返回*评估结果*后,我们将 `ResultBox.Text` 属性设置为保存表示*评估结果*的序列化 JSON 值,然后调用 `Invoke` 方法来执行得分最高的意图。

private void InputBox_OnKeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Return)
    {
        EvaluateButton_OnClick(null, null);
    }
}

上面的代码在 `InputBox` 中按下 **Enter** 键时简单地调用 `EvaluateButton_OnClick` 方法。

创建对话框 (Dialogs)

终于!这是我们一直在等待的时刻。到目前为止,我们一直在为数据库机器人进行前期准备。现在加入进来,让我们开始实现一个基本的 `dialog` 类。

`Dialog` 实际上就是一组相关的 `intents` 的集合。那么 `Intent` 是什么呢?

`Intent` 就是 Intent!好吧,这可能听起来不对。让我解释一下。`Intent` 是将用户输入映射到可执行操作的。所以简单来说,在 Oscova 中,`Intent` 是当用户输入匹配表达式时调用的方法。

另一方面,`Expression` 是一个模式或示例,取决于您的编写方式,它类似于用户输入。

Oscova 的评分系统将用户输入与所有 *expressions* 进行比较。得分高的表达式及其意图将作为 *评估结果* 的一部分返回。

好了,代码老手们,现在我们将创建我们的第一个对话框。

  • 在 **解决方案资源管理器** 中右键单击项目,选择 **添加**,然后选择 **新建文件夹**。
  • 将文件夹命名为 `Dialogs`。
  • 右键单击文件夹,选择 **添加**,然后选择 **类…**
  • 将类命名为 `DatabaseEnquiryDialog` 并选择 **添加**。

为了将 `DatabaseEnquiryDialog` 转换为 Oscova 对话框,您所要做的就是让该类继承自 Oscova 的 `Dialog` 类,如下所示:

class DatabaseEnquiryDialog: Dialog
{
}

创建意图 (Intents)

我们的第一个意图将非常简单。我们只是希望当用户说类似“**Rick 的年龄是多少?**”(`Rick` 是一个 `employee`)时,机器人能返回相应 `Age` 列的值。

我们将意图命名为 `PropertyEnquiry`。为此,我们首先向 `DatabaseEnquiryDialog` 类添加一个 `void` 方法,如下所示:

public void PropertyEnquiry(Context context, Result result)
{
}

关于上面代码的一些信息:

  • 意图方法必须始终具有 `public` 修饰符。
  • 一个意图可以接受 0、1(`Context` 或 `Result`)或 2(`Context` 和 `Result`)个参数。
  • 一个意图必须至少有一个 Expression。接下来介绍。

为意图添加表达式 (Expressions)

通过 `Expression` 属性向意图添加表达式。有两种主要类型的表达式:

  • 示例表达式 (Example Expressions) - 我们只编写一个示例输入,并可以选择用大括号注解实体。
  • 模板表达式 (Template Expressions) - 我们编写用户输入,但不是注解,而是直接指定实体类型。

对于“**What is the age of Rick?**”的示例表达式看起来会像这样:

[Expression("What is the {age} of {Rick}?")]
[Entity("property")]
[Entity("name")]

第一个 `Entity` 属性现在将注解的词 `age` 链接到实体类型 `property`,而词 `Rick` 被链接到实体类型 `name`。

注意:实体属性的顺序很重要,因为注解的单词或短语的顺序与 `Entity` 属性的顺序对齐。

对于“**What is the age of Rick?**”的模板表达式看起来会像这样:

[Expression("What is the @property of @name?")]

与*示例*表达式不同,*模板*表达式编写起来更简单,因为通过直接指定实体类型名称(以 `@` 开头)来避免注解。唯一的权衡是代码的可读性。

为了本文的方便,我们将向 `PropertyEnquiry` 意图添加一个*示例*表达式。

[Expression("What is the {age} of {Rick}?")]
[Entity("property")]
[Entity("name")]
public void PropertyEnquiry(Context context, Result result)
{
}

注意:所有 Oscova 属性都可以在 `Syn.Bot.Oscova.Attributes` 命名空间下找到。

现在,如果一个意图没有任何作用,那有什么用呢?这个意图必须为我们做两件事:

  1. 从数据库中选择 `Rick` 的 `age` 列。
  2. 显示一条消息,说明选择了 `employee` 的哪个 `property`。

为此,我们将以下内容添加到方法体中:

public void PropertyEnquiry(Context context, Result result)
{
    var name = result.Entities.OfType("name").Value;
    var property = result.Entities.OfType("property").Value;

    var utility = context.SharedData.OfType<DatabaseUtility>();
    utility.PropertyByName(name, property);

    result.SendResponse($"{property} of Employee \"{name}\".");
}

上面的代码首先获取 `name` 实体的匹配值,然后获取 `property` 实体的匹配值。所有提取的实体都存储在 `Result.Entities` 集合中。碰巧的是,集合中的实体通常按照它们在用户输入中出现的顺序排列。

接下来,我们获取共享的 `DatabaseUtility` 对象并调用 `PropertyByName` 方法。此方法通过选择 `Employees` 表中名称等于指定 `name` 值的指定*行*来更新我们 WPF 应用程序中的 `DataGrid`。

最后,通过调用 `Result.SendResponse` 方法,我们传递文本响应。此响应,如 `MainWindow` 构造函数中所见,将更改 `ResponseLabel` 的*内容*。

整体的意图代码看起来应该像这样:

[Expression("What is the {age} of {Rick}?")]
[Entity("property")]
[Entity("name")]
public void PropertyEnquiry(Context context, Result result)
{
    var name = result.Entities.OfType("name").Value;
    var property = result.Entities.OfType("property").Value;

    var utility = context.SharedData.OfType<DatabaseUtility>();
    utility.PropertyByName(name, property);

    result.SendResponse($"{property} of Employee \"{name}\".");
}

测试意图

在继续测试 `Intent` 之前,我们需要将 Dialog 添加到 Oscova。在 `MainWindow` 构造函数中,在 `//Dialogs go here` 注释下方,添加以下内容:

Bot.Dialogs.Add(new DatabaseEnquiryDialog());

好了,现在按 **F5** 运行应用程序,并在 `InputBox` 中输入:

  • Rick 的年龄是多少?
  • Ahmed 的薪水是多少?

如果输出符合预期,恭喜!您已成功创建了第一个意图。

注意:通过在*表达式*中注解值,我们消除了为不同属性编写冗余表达式的需要。

顺便说一句,如果您点击 **JSON Result** 选项卡,您应该会看到*评估结果*的序列化版本。

{
  "query": "WHAT IS THE AGE OF RICK?",
  "sessionId": "78a8765f-3705-442d-a4ca-fe4dbd4c2e04",
  "intents": [
    {
      "intent": "DatabaseEnquiryDialog.PropertyEnquiry",
      "score": 0.9985714285714286
    },
    {
      "intent": "DatabaseEnquiryDialog.EmployeeName",
      "score":  0.59047619047619047
    }
  ],
  "entities": [
    {
      "entity": "AGE",
      "type": "property"
    },
    {
      "entity": "Rick",
      "type": "Name"
    }
  ],
  "contexts": []
}

注意:在不同的机器人设置中,意图得分可能会有所不同。

更多意图

在进入创建上下文意图的下一阶段之前,我们将添加一些更多带有*模板*表达式的意图,这仅仅是为了更好地理解。

输入模式意图:*CEO 的薪水是多少?*

[Expression("What is the @property of @role")]
[Expression("@property of @role")]
public void PropertyByRole(Context context, Result result)
{
    var role = result.Entities.OfType("role").Value;
    var property = result.Entities.OfType("property").Value;

    var utility = context.SharedData.OfType<DatabaseUtility>();
    utility.PropertyByRole(role, property);

    result.SendResponse($"{property} of \"{role}\".");
}

输入模式意图:*王是谁?*

[Expression("Find employee with the name @name")]
[Expression("Who is @name?")]
public void EmployeeName(Context context, Result result)
{
    var name = result.Entities.OfType("name").Value;

    var utility = context.SharedData.OfType<DatabaseUtility>();
    utility.EmployeeByName(name);

    result.SendResponse($"Employee(s) with the name {name}.");
}

输入模式意图:*管理员是谁?*

[Expression("Find employee whose role is @role")]
[Expression("Who is the @role?")]
public void EmployeeRole(Context context, Result result)
{
    var role = result.Entities.OfType("role").Value;

    var utility = context.SharedData.OfType<DatabaseUtility>();
    utility.EmployeeByRole(role);

    result.SendResponse($"Employee(s) with job role \"{role}\".");
}

上下文意图 (Contextual Intents)

在本节中,我们将学习两个新概念:

  1. 上下文意图 (Contextual Intents)
  2. 使用预构建的系统实体

虽然 Oscova 提供了数十种预构建的系统实体,但我们将只使用其中两种。

  1. 右键单击 `Dialogs` 文件夹,选择 **添加**,然后选择 **类…**
  2. 将类命名为 `DatabaseUpdateByNameDialog` 并选择 **添加**。

与我们之前的对话框一样,让 `DatabaseUpdateByNameDialog` 继承自基类 `Dialog`,如下所示:

class DatabaseUpdateByNameDialog : Dialog
{
}

此对话框内的意图结构如下:

  • 一个允许用户更改员工属性(年龄、薪水)值的意图。
  • 肯定意图 - 如果用户说 **Yes**,则提交更改到数据库。
  • 否定意图 - 如果用户说 **No**,则取消提交操作。

什么是上下文 (Context)?

上下文是一个 `string` 值,表示用户输入的当前上下文。在 Oscova 中,上下文有名称和生命周期。生命周期通常是上下文持续的请求次数。

为了更好地处理*常量*字符串值,我们将首先创建一个 `static` 类来保存一个我们将用作上下文项名称的常量 `string` 值。

向项目添加一个新类并将其命名为 `DatabaseContext`。

static class DatabaseContext
{
    public const string ConfirmUpdateByName = "ConfirmUpdateByName";
}

现在,第一个允许用户修改数据库的意图。双击 `DatabaseUpdateByNameDialog.cs` 并添加以下代码:

[Expression("@set @property of @name to @sys.number")]
public void ChangePropertyOfName(Context context, Result result)
{
    context.Add(DatabaseContext.ConfirmUpdateByName);

    var property = result.Entities.OfType("property");
    var name = result.Entities.OfType("name");
    var number = result.Entities.OfType<NumberEntity>();

    context.SharedEntities.Add(property);
    context.SharedEntities.Add(name);
    context.SharedEntities.Add(number);

    var propertyString = property.Value.ToLower();
    if (propertyString != "age" && propertyString != "salary")
    {
        result.SendResponse($"{property} of {name} is readonly.");
        return;
    }

    result.SendResponse
    ($"Are you sure that you want to change {property} of {name} to {number}?");
}

上面的代码中的 `Expression` 支持以下命令:

  • 将 Mark 的年龄改为 43
  • 将 Krishna 的薪水设置为 17000

在模板表达式中,值 `@sys.number` 是匹配数字的系统实体类型。有大量的系统实体可供开发人员使用。我鼓励您查阅 Oscova 的 API 文档以获取更多详细信息。

注意 `context.Add(DatabaseContext.ConfirmUpdateByName);`,通过将 `string` 值添加到上下文,机器人将在其下一个请求中首先搜索指定上下文中的意图。

因为在下一次意图调用时,`result` 中的当前实体会被清除,所以我们使用 `Context.SharedEntities` 属性将当前实体传递给下一个意图。

最后,当用户发送上述命令时,我们会向用户发送一个响应,请求确认。

让我们先处理更简单的用户*否定*跟进响应。为此,我们将创建一个如下所示的意图:

[Expression("{no}")]
[Entity(Sys.Negative)]
[Context(DatabaseContext.ConfirmUpdateByName)]
public void ChangePropertyByNameDeclined(Context context, Result result)
{
    context.SharedEntities.Clear();
    result.SendResponse("Operating cancelled.");
}

上面意图有两个值得注意的地方:

首先,`[Entity(Sys.Negative)]`,一个 `Entity` 属性,指定了类型为 `@sys.negative` 的系统实体,它匹配 `No`、`Nope` 等。您可以在 `Sys` 类的常量字段中找到所有实体类型。

其次,`[Context(DatabaseContext.ConfirmUpdateByName)]` 明确指定此意图仅在 `ConfirmUpdateByName` 上下文下可用。

接下来是用户*肯定*的跟进响应。我们创建以下意图:

[Expression("{Yes}")]
[Entity(Sys.Positive)]
[Context(DatabaseContext.ConfirmUpdateByName)]
public void ChangePropertyConfirmed(Context context, Result result)
{
    var property = context.SharedEntities.OfType("property");
    var name = context.SharedEntities.OfType("name").Value;
    var number = context.SharedEntities.OfType<NumberEntity>().Value;

    var utility = context.SharedData.OfType<DatabaseUtility>();
    utility.UpdatePropertyByName(name, property.Value, 
                                 number.ToString(CultureInfo.InvariantCulture));

    result.SendResponse($"{property} of {name} changed to {number}");
    context.SharedEntities.Clear();
}

上面意图的表达式将 `yes` 注解为 `@sys.positive` 实体类型,以捕获肯定的跟进响应。与 `ChangePropertyByNameDeclined` 意图一样,我们指定此意图也必须在 `ConfirmUpdateByName` 上下文下可用。

在方法体内部,我们首先检索之前添加到 `SharedEntities` 集合的实体,并将值传递给 `DatabaseUtility.UpdatePropertyByName` 函数。

在进行提交后,我们发送一个文本响应,说明已完成请求的更改,并清除 `SharedEntities` 集合。

有了上面的意图,如果用户说 `Yes` 或类似的短语,更改就会提交到 `Employees` 数据库。

测试意图

我们将重复之前的对话框操作,将 `DatabaseUpdateByNameDialog` 添加到 `Bot.Dialogs` 集合中,方法是在 `MainWindow` 构造函数中 `//Dialogs go here` 注释下方放置以下代码。

Bot.Dialogs.Add(new DatabaseUpdateByNameDialog());

按 **F5** 运行应用程序,然后输入 **Change the salary of Vincent to 24000**(将 Vincent 的薪水改为 24000)。

如果一切顺利,您应该会看到以下问题:

您确定要将 Vincent 的薪水改为 24000 吗?

如果您说 `yeah`,更改将提交到数据库,否则操作将被取消。

好了,各位!

有疑问或反馈?请在评论区告诉我。感谢阅读!

关注点

自然语言处理:为了使本文不会过于冗长,我没有详细阐述将机器人连接到词汇数据库或在训练机器人之前加载词嵌入。根据官方文档,这对于提高机器人的自然语言处理能力非常重要。您可以在 官方教程页面 上找到关于如何进行此操作的良好阅读材料。

我之所以倾向于 Oscova,是因为它能够构建智能机器人,并且无需担心技术细节,因为 Oscova 基本上会处理所有事情。Oscova 极其灵活的可插拔架构使开发人员能够完全自定义自然语言处理的各个方面,这在复杂的机器人项目中非常有用。

此外,由于本文采用了通俗易懂的方法,我在此文中避免了深入探讨太多细节。官方文档页面上有大量教程。我鼓励任何爱好者在尝试构建生产级产品之前,先仔细阅读这些教程,以全面掌握 Oscova 的架构。

历史

  • 2017年1月17日:初版文章
© . All rights reserved.