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

使用 SIML 的自然语言数据库接口

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2015年5月14日

CPOL

10分钟阅读

viewsIcon

46724

downloadIcon

1028

使用 SIML(一种专为数字助理设计的语言)创建 SQL 数据库的自然语言界面。

必备组件

如果您不熟悉 SIML,我建议您阅读以下文章:

在继续阅读本文之前,您**必须**了解 C#、SQL 和 SIML(发音为 *si mal*)。

**注意**:如果您没有通读上述文章,您可能无法理解本文的内容。

除非另有说明,本文中“自然语言界面”、“NLI”、“LUI”或“NLUI”可互换使用。

设置

以下是我们的想法:

  • 使用 Visual Studio,我们将创建一个简单的 **WPF** 应用程序(包含一个 `TextBox`、一个**评估** `Button` 和一个 `DataGrid`)。
  • 启动时,应用程序将创建一个简单的数据库,其中包含一个 `Employees` 表,并填充 10 名员工及其详细信息(如 `ID`、`Name`、`Age`、`Salary`、`Job`)。
  • 然后,我们将一个 `SIML` 项目加载到我们的 `SynBot` 类对象中,并从数据库中传入一些重要值。
  • 稍后,我们将创建一个 SQL 适配器,它将接收 SQL `string` 并对其进行评估。
  • 最后,我们将使用 **SIML** 知识库与数据库进行交互。

首先,创建一个名为 `NLI-Database` 的 WPF 应用程序。我要求您将项目命名为 `NLI-Database`,因为后面的一些代码片段可能会使用命名空间 `NLI_Database`。

在开始之前,我们必须在项目中添加对 `Syn.Bot` 类库的引用。为此,在 Visual Studio 中,单击 **TOOLS** -> **NuGet Package Manager** -> **Package Manager Console** 并键入:

Install-Package Syn.Bot

完成之后,我们的项目中将包含 `Bot` 库。请注意,这不仅仅是一个 `Bot` 库,它还是一个符合规范的 SIML 解释器。它是一个独立的库,因此您无需在此处进行任何繁琐的操作。

现在是数据库。我们也将导入 SQLite。

同样,在 *Package Manager Console* 中,键入:

Install-Package System.Data.SQLite

太棒了!希望您所有的引用都设置正确了。

C# 编码

数据库实用程序

由于我的懒惰阈值早已达到,我们来创建一个简单的实用程序类 `DatabaseUtilty`,我们将在应用程序启动时使用它来创建一个简单的 `Employees` 表并填充一些数据。在您的项目中添加一个新的类文件,将其命名为 `DatabaseUtility` 并添加以下代码行:

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

   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, 
       Job 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, 'Carl', 'Support', 20, 20000)");
       ExecuteCommand("INSERT INTO EMPLOYEES VALUES(9, 'Marco', 'Tech', 24, 34000)");
       ExecuteCommand("INSERT INTO EMPLOYEES VALUES(10, 'Craig', 'Admin', 25, 18000)");
   }
   public void ExecuteCommand(string commandText)
   {
       Command = new SQLiteCommand(Connection) {CommandText = commandText};
       Command.ExecuteNonQuery();
   }
   public void Close()
   {
       Command.Dispose();
       Connection.Dispose();
   }
}

上述代码中的 `Initialize` 方法检查文件 `EmployeesTable.db` 是否存在。如果存在,它将删除该文件并创建一个新文件。完成之后,代码将一些虚拟 `employee` 详细信息添加到 `Employees` 表中。

SQL 适配器

是时候为 SQL 创建我们的第一个 SIML 适配器了。在您的 *Solution* 中创建一个新文件夹并将其命名为 *Adapter*。在此文件夹中,添加一个新类文件并将其命名为 `SqlAdapter`。`SqlAdapter` 类必须实现 `IAdapter` 接口(在 `Syn.Bot` 库中找到),正是这个接口将您的应用程序与 SIML 连接起来。

public class SqlAdapter : IAdapter
{
    private readonly MainWindow _window;
    public SqlAdapter(MainWindow window)
    {
        _window = window;
    }
    public bool IsRecursive { get { return true; } }
    public XName TagName { get { return Specification.Namespace.X + "Sql"; } }
    public string Evaluate(Context parameter)
    {
        _window.UpdateDataGrid(parameter.Element.Value);
        return string.Empty;
    }
}

是的,`SqlAdapter` 就这么多。那么它有什么作用呢?

嗯,如果您阅读过我之前写的 SIML 文章,您就会知道 SIML 使用符合 XML 的结构。这个适配器允许我们在 SIML 代码中使用 `Some SQL here`。在解释器评估时,它将调用上述 `Adapter` 代码中的 `Evaluate` 函数。您可能会注意到,上述代码中的 `Evaluate` 函数正在对 `MainWindow` 类中可能存在的 `UpdateDataGrid` 函数进行奇怪的调用。我将在本文后面讨论这一点。

列集

SIML 中的 `Set` 是单词或句子的集合。一旦创建了一个具有唯一名称的集合,我们就可以使用该集合的名称来指定我们希望在 SIML 模式中捕获的单词集合。

一个名为 “`EMP-NAME`” 的集合将允许我们捕获员工的姓名。同样,一个名为 “`EMP-JOB`” 的集合将允许我们捕获 `Employees` 表中员工扮演的所有独特角色。

继续在您的解决方案中创建一个文件夹并将其命名为 *Sets*。向其中添加一个新类文件并将其命名为 `NameSet`。`NameSet` 类必须实现 `ISet` 接口(在 `Syn.Bot` 库中找到)。

public class NameSet : ISet
{
    private readonly HashSet<string> _nameSet;
    public NameSet(DatabaseUtility databaseUtility)
    {
        _nameSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        databaseUtility.Command.CommandText = "SELECT * FROM EMPLOYEES";
        var reader = databaseUtility.Command.ExecuteReader();
        while (reader.Read())
        {
            _nameSet.Add(reader["name"].ToString());
        }
        reader.Close();
    }
    public bool Contains(string item)
    {
        return _nameSet.Contains(item);
    }
    public string Name { get { return "Emp-Name"; }}
    public IEnumerable<string> Values { get { return _nameSet; } }
}

每个 SIML `Set` 都具有唯一的名称并返回可枚举的 `string` 值。由于 SIML 集合不允许包含重复值,我们将使用 `HashSet` 来存储 `Employees` 表中所有 `employee` 的姓名。是的,公司中两名或更多员工可能具有相似的姓名,但当 SIML 集合的唯一目的是促进模式匹配时,这并不重要。

就像 `NameSet` 一样,我们将为 `ID`、`Job`、`Age` 和 `Salary` 创建另外四个 `Set`。

年龄集(AgeSet)

public class AgeSet : ISet
{
    private readonly HashSet<string> _ageSet;
    public AgeSet(DatabaseUtility databaseUtility)
    {
        _ageSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        databaseUtility.Command.CommandText = "SELECT * FROM EMPLOYEES";
        var reader = databaseUtility.Command.ExecuteReader();
        while (reader.Read())
        {
            _ageSet.Add(reader["Age"].ToString());
        }
        reader.Close();
    }
    public bool Contains(string item)
    {
        return _ageSet.Contains(item);
    }
    public string Name { get { return "Emp-Age"; }}
    public IEnumerable<string> Values { get { return _ageSet; } }
}

ID集(IdSet)

public class IdSet : ISet
{
    private readonly HashSet<string> _idSet;
    public IdSet(DatabaseUtility databaseUtility)
    {
        _idSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        databaseUtility.Command.CommandText = "SELECT * FROM EMPLOYEES";
        var reader = databaseUtility.Command.ExecuteReader();
        while (reader.Read())
        {
            _idSet.Add(reader["ID"].ToString());
        }
        reader.Close();
    }
    public bool Contains(string item)
    {
        return _idSet.Contains(item);
    }
    public string Name { get { return "Emp-ID"; }}
    public IEnumerable<string> Values { get { return _idSet; } }
}

工作集(JobSet)

public class JobSet : ISet
{
    private readonly HashSet<string> _jobSet;
    public JobSet(DatabaseUtility databaseUtility)
    {
        _jobSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        databaseUtility.Command.CommandText = "SELECT * FROM EMPLOYEES";
        var reader = databaseUtility.Command.ExecuteReader();
        while (reader.Read())
        {
            _jobSet.Add(reader["Job"].ToString());
        }
        reader.Close();
    }
    public bool Contains(string item)
    {
        return _jobSet.Contains(item);
    }
    public string Name { get { return "Emp-Job"; }}
    public IEnumerable<string> Values { get { return _jobSet; } }
}

薪水集(SalarySet)

public class SalarySet : ISet
{
    private readonly HashSet<string> _salarySet;
    public SalarySet(DatabaseUtility databaseUtility)
    {
        _salarySet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        databaseUtility.Command.CommandText = "SELECT * FROM EMPLOYEES";
        var reader = databaseUtility.Command.ExecuteReader();
        while (reader.Read())
        {
            _salarySet.Add(reader["Salary"].ToString());
        }
        reader.Close();
    }
    public bool Contains(string item)
    {
        return _salarySet.Contains(item);
    }
    public string Name { get { return "Emp-Salary"; }}
    public IEnumerable<string> Values { get { return _salarySet; } }
}

GUI

GUI 应该如下图所示:

有一个输入框、一个**评估**按钮和一个 `DataGrid`。目前请*忽略* **示例**选项卡。

在 *MainWindow.xaml* 的主 `Grid` 中,添加以下内容。是的!其中会包含一些未定义的符号,但我们会在您完成输入后立即修复它们。

<TabControl>
    <TabItem Header="Interaction">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="35"/>
                <RowDefinition Height="53*"/>
                <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="ExecuteButton_OnClick"/>
            </Grid>
            <Label Grid.Row="1"  Name="ResponseLabel" 
             Content="No Response Yet" VerticalContentAlignment="Center"/>
            <DataGrid  Name="EmployeeGrid" Grid.Row="2"  FontSize="14" />
        </Grid>
    </TabItem>
</TabControl>

太棒了!您已经添加了构成 GUI 的 XAML 代码。现在用以下代码替换代码隐藏文件:

using System.Data;
using System.Data.SQLite;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Xml.Linq;
using NLI_Database.Adapter;
using NLI_Database.Sets;
using Syn.Bot;

namespace NLI_Database
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow
    {
        public DatabaseUtility DatabaseUtility { get; private set; }
        public SynBot Bot { get; private set; }
        public MainWindow()
        {
            InitializeComponent();
            Bot = new SynBot();
            DatabaseUtility = new DatabaseUtility();
            DatabaseUtility.Initialize();
            UpdateDataGrid("SELECT * From Employees");
            Bot.Sets.Add(new NameSet(DatabaseUtility));
            Bot.Sets.Add(new JobSet(DatabaseUtility));
            Bot.Sets.Add(new SalarySet(DatabaseUtility));
            Bot.Sets.Add(new AgeSet(DatabaseUtility));
            Bot.Sets.Add(new IdSet(DatabaseUtility));
            Bot.Adapters.Add(new SqlAdapter(this));
            var simlFiles = Directory.GetFiles
                            (Path.Combine(Directory.GetCurrentDirectory(), "SIML"), 
                            "*.siml", SearchOption.AllDirectories);
            foreach (var simlDocument in simlFiles.Select(XDocument.Load))
            {
                Bot.AddSiml(simlDocument);
            }
        }

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

        private void ExecuteButton_OnClick(object sender, RoutedEventArgs e)
        {
            var result = Bot.Chat(string.IsNullOrEmpty(InputBox.Text) ? 
                                  "clear" : InputBox.Text);
            ResponseLabel.Content = result.BotMessage;
            InputBox.Clear();
        }

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

上述代码中的**构造函数**实例化 `Bot` 和 `DatabaseUtility` 变量,调用 `DatabaseUtility` 对象的 `Initialize` 方法,将 `SqlAdapter` 和之前创建的 SIML Sets 添加到 `Bot` 对象中,最后加载应用程序**根**目录中 *SIML* 文件夹下找到的所有 SIML 文件。

`UpdateDataGrid` 方法接收一个 SQL `string` 并对其进行评估。评估完成后,它会刷新 `EmployeeGrid` 的 `ItemSource` 属性。

SIML 编码

在继续之前,我假设您已经至少阅读了我撰写的第一个 SIML 文章

由于模式识别超出了本文的范围,我将展示一些在 SIML 中使用的简单 SQL 命令。但无需担心,因为本文附带的项目预先声明了许多模式,可以通过在 Chatbot Studio 中打开 SIML 项目来参考。*这也是我将 SIML 项目直接放在 SIML 目录中,而没有使用 SIML 包的原因之一。*

在应用程序的*根*目录下创建一个名为 *SIML* 的新文件夹。它可能是 *Bin/Debug* 或 *Bin/Release* 目录,具体取决于您的项目*输出路径*设置。

如果您还没有 **Chatbot Studio**,请从这里下载。按 **Ctrl+Shift+N** 创建一个新项目。填写所需详细信息并选择 **English Minimum** 作为默认模板。将项目保存到您刚刚创建的 *SIML* 文件夹中。

点击文件 **Hello Bot**,您将看到一个简单的 SIML 文档。此文档只有一个 SIML 模型。它匹配模式 **Hello Bot** 并生成响应 **Hello User!**。通过选择并按 **Delete** 删除此模型。

通过右键单击 `<Siml>` 标签的 **>** 之前并选择 **Insert** -> **Attributes** -> **xmlns:x**,向 SIML 文档的根元素添加一个新的命名空间 **x**。

将插入一个命名空间 `xmlns:x="http://syn.co.in/2014/siml#external"`。这个命名空间非常重要,因为我们将在 SIML 代码中大量使用 **x** 命名空间。

简单的员工相关查询

现在按 **Alt+M** 插入一个新的 SIML 模型,并在 Pattern 标签内添加 **What is the age of [EMP-NAME]**。

以上模式匹配:

  • Rick 的年龄是多少?
  • Lincoln 的年龄是多少?
  • Jorge 的年龄是多少?等等...

如果您还记得,我们之前创建了一个派生自 `ISet` 接口的 `NameSet` 类。我们选择返回的 `Set` 的名称是 `Emp-Name`。在 SIML 中,您可以通过将集合名称括在方括号中来指定一个集合(在 SIML 模式中)。这正是我们在这里所做的。

在 `Response` 元素类型中输入 `Age of Employee <Match />`,然后是:

<x:Sql>SELECT DISTINCT Age FROM Employees WHERE UPPER(Name) 
LIKE UPPER('%<Match />%')</x:Sql>

您的新 SIML 模型现在应该看起来与以下内容类似:

<Model>
  <Pattern>WHAT IS THE AGE OF [EMP-Name]</Pattern>
  <Response>
    Age of the employee <Match />.
    <x:Sql>SELECT DISTINCT Age FROM Employees WHERE UPPER(Name) 
           LIKE UPPER('%<Match />%')</x:Sql>
   </Response>
</Model>

按 **Ctrl+S** 保存文档。

现在继续启动您的应用程序并输入“*What is the age of Rick*”,输出应该如下图所示:

真可爱,再来一些相同类型查询的模式怎么样?将您的第一个 SIML 模型更改为...

<Model>
  <Pattern>
    <Item>WHAT IS THE AGE OF [EMP-NAME]</Item>
    <Item>HOW OLD IS [EMP-NAME]</Item>
    <Item>$ AGE OF [EMP-NAME]</Item>
  </Pattern>
  <Response>
    Age of the employee <Match />.
    <x:Sql>SELECT DISTINCT Age FROM Employees WHERE UPPER(Name) 
           LIKE UPPER('%<Match />%')</x:Sql></Response>
</Model>

现在,以下查询将产生相同的结果:

  • Rick 多少岁?
  • Rick 的年龄是多少?
  • **HAKUNA MATATA** Rick 的年龄?

您现在可以继续为 `Salary`、`Job` 和 `ID` 相关的查询创建许多模式。(本文附带的下载包含许多预定义的模式。)

这是另一个示例 SIML 代码,它响应诸如 *Who is Rick?*、*Who is Jorge?* 之类的问题:

<Model>
  <Pattern>WHO IS [EMP-NAME]</Pattern>
  <Response>
    Employee(s) with the name <Match />.
    <x:Sql>SELECT * FROM Employees WHERE UPPER(Name) LIKE UPPER('%<Match />%')</x:Sql>
  </Response>
</Model>

根据谓词列出信息

现在我们来创建一个 `Operator` 集合。

正如我之前提到的,SIML 集合是单词或句子的独特集合,它有助于模式匹配。在 **File Explorer**(在 Chatbot Studio 中),选择 *Sets* 文件并添加以下内容:

<Set Name="operator">
  <Item>equal to</Item>
  <Item>less than</Item>
  <Item>greater than</Item>
  <Item>less than or equal to</Item>
  <Item>greater than or equal to</Item>
</Set>

现在点击 **Maps** 并添加以下内容:

<Map Name="operator">
  <MapItem Content="equal to" Value="=" />
  <MapItem Content="less than" Value="&lt;" />
  <MapItem Content="greater than" Value="&gt;" />
  <MapItem Content="less than or equal to" Value="&lt;=" />
  <MapItem Content="greater than or equal to" Value="&gt;=" />
</Map>

另一方面,SIML `Map` 允许我们在运行时将给定值映射到其他值。在上面的代码中,**等于**被*映射*到符号 **=**,**小于**被映射到 <,**大于**被映射到符号 >,等等...

现在让我们添加一个新的 SIML 模型,该模型将允许我们获取年龄或薪水大于、小于或等于某个指定值的员工的详细信息。

  • 列出所有年龄小于 40 岁的员工
  • 列出所有工资高于 18000 的员工
<Model>
  <Pattern>LIST ALL EMPLOYEES * (AGE|SALARY) IS [OPERATOR] *</Pattern>
  <Response>
    <x:Sql>select * from Employees where <Match Index="2" />
    <Map Get="operator"><Match Index="3" /></Map><Match Index="4" /></x:Sql>
  </Response>
</Model>

按 **Ctrl+S** 保存文档,运行 WPF 应用程序并键入 *列出所有年龄大于 30 岁的员工*。

现在尝试 *列出所有薪资低于 30000 的员工*。

更改数据库中的信息

现在我们将尝试根据员工的 `ID`(使用姓名会很荒谬,因为公司中两名或更多员工可能具有相同的姓名)和新的年龄值来更改员工的 `age`。

将以下 SIML 模型添加到您的 SIML 文档中。

<Model>
  <Pattern>(CHANGE|SET|UPDATE) THE AGE OF ID [EMP-ID] TO *</Pattern>
  <Response>
    Age of ID <Match Index="2" /> has now changed to <Match Index="3" />.
    <x:Sql>UPDATE EMPLOYEES SET AGE=<Match Index="3" /> 
     WHERE ID=<Match Index="2" />;</x:Sql><x:Sql>SELECT * FROM EMPLOYEES 
     WHERE ID=<Match Index="2" />;</x:Sql>
  </Response>
</Model>

保存文档,重启 WPF 应用程序并尝试输入 *将 ID 3 的年龄更改为 34*。

好了,这就是这个初始版本的所有内容。您最好自己尝试和试验 SIML。无论如何,您可以根据需要使用本文附带的项目。

关注点

使用 SIML 作为接口的优点是,要更新模式,您无需对应用程序代码进行任何更改。您的 SIML 自然语言接口和数据库之间的抽象层非常坚固,并且在许多场景下都表现良好。

即使您因某种原因更改了 `Employees` 表的结构,您的 SIML 代码仍然可以重用。最重要的是,SIML 解释器是平台独立的,因此请不要犹豫在 Linux 或 Mac 上的 Mono 中尝试相同的操作。

友情提示。不要盲目地将我的设置连接到您的某个数据库。您可能会搞砸事情。务必备份您的数据库,然后再操作敏感的东西,如自然语言接口。我自己也搞砸了很多次,才弄出一个可用的原型。

除了作为数据库的 NLI 外,此设置*可能*模拟一些搜索引擎功能。在本文附带的示例项目中,如果您只输入 `employee` 的 `Name`、`ID`、`Age` 或 `Salary`,您将获得与给定值匹配的行。

历史

  • 2015 年 5 月 14 日星期四 - 初版
© . All rights reserved.