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





5.00/5 (8投票s)
使用 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 代码中使用 `
列集
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="<" />
<MapItem Content="greater than" Value=">" />
<MapItem Content="less than or equal to" Value="<=" />
<MapItem Content="greater than or equal to" Value=">=" />
</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 日星期四 - 初版