Compare commits

...

10 Commits

Author SHA1 Message Date
8b604ebb9c add: build and gallery in README
Some checks failed
Dotnet test / test (push) Has been cancelled
Build Frontend Docker Image / build (push) Has been cancelled
2024-09-19 21:19:51 +08:00
9bdbae3e44 add: README.md 2023-12-02 19:34:59 +08:00
81cd5aa06f add: 语法测试4 2023-11-26 11:34:42 +08:00
79962727ba add: 检测递归转移单元测试 2023-11-26 11:27:05 +08:00
86551f244f fix: 空转移时防止递归转移 2023-11-26 11:26:37 +08:00
bd1cc08bba add: 修改部分注释 2023-11-25 18:34:58 +08:00
da71e8d4df add: Grammar2测试 2023-11-20 13:32:44 +08:00
fff3a4ed96 add: dotnet test workflow 2023-11-19 21:31:44 +08:00
baf68f3676 add: Katheryne机器人单元测试 2023-11-19 21:24:41 +08:00
222bd715e7 fix: 在Dockerfile中使用.net 8 2023-11-19 13:23:21 +08:00
40 changed files with 672 additions and 5 deletions

View File

@ -0,0 +1,10 @@
name: Dotnet test
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: https://git.rrricardo.top/actions/checkout@v3
- run: dotnet test

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/aspnet:7.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY ./publish/ .
ENTRYPOINT ["dotnet", "Frontend.dll"]

View File

@ -9,8 +9,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
@ -24,5 +25,15 @@
<ItemGroup>
<ProjectReference Include="..\Katheryne\Katheryne.csproj" />
</ItemGroup>
<Target Name="CopyTestFiles" AfterTargets="CoreCompile">
<ItemGroup>
<TestTxt Include="Katheryne/**/*.*"/>
</ItemGroup>
<Copy SourceFiles="@(TestTxt)"
DestinationFolder="$(OutputPath)/%(RecursiveDir)"
/>
</Target>
</Project>

View File

@ -0,0 +1 @@
你好

View File

@ -0,0 +1,5 @@
坏了,被你发现默认机器人了。
使用这个粪机器人,怎么能得高分呢?
必须要出重拳!
啊对对对。
我不到啊。

View File

@ -0,0 +1,14 @@
robotName: 凯瑟琳
stages:
- name: start
answer: 向着星辰和深渊!欢迎来到冒险家协会。
transformers:
- pattern: .*?
nextStageName: running
- name: running
answer: 对不起,做不到。
transformers:
- pattern: .*?
nextStageName: running
beginStageName: start

View File

@ -0,0 +1,3 @@
你说得对
但是
原神

View File

@ -0,0 +1,5 @@
向着星辰和深渊!欢迎来到冒险家协会。
对不起,做不到。
对不起,做不到。
对不起,做不到。
再见。

View File

@ -0,0 +1,36 @@
robotName: 凯瑟琳
stages:
- name: running
answer: 向着星辰和深渊!欢迎来到冒险家协会。
transformers:
- pattern: 领取每日委托奖励
nextStageName: daily-task-question
- pattern: 你好|您好|[Hh]ello
nextStageName: hello
- pattern: .*?
nextStageName: unknown
- name: daily-task-question
answer: 冒险家今日完成的任务是?
transformers:
- pattern: (.*)
nextStageName: daily-task
- name: daily-task
answer: 感谢冒险家完成了“$1”, 这是你的奖励。
transformers:
- pattern:
nextStageName: running
- name: hello
answer: 你好,我是冒险家协会的接待员凯瑟琳。
transformers:
- pattern:
nextStageName: running
- name: unknown
answer: 对不起,做不到。
transformers:
- pattern:
nextStageName: running
beginStageName: running

View File

@ -0,0 +1,4 @@
你好
I asdasdasdfsavnakjnhvas;dvf
领取每日委托奖励
sCSJDASKJDNVF

View File

@ -0,0 +1,9 @@
向着星辰和深渊!欢迎来到冒险家协会。
你好,我是冒险家协会的接待员凯瑟琳。
向着星辰和深渊!欢迎来到冒险家协会。
对不起,做不到。
向着星辰和深渊!欢迎来到冒险家协会。
冒险家今日完成的任务是?
感谢冒险家完成了“sCSJDASKJDNVF”, 这是你的奖励。
向着星辰和深渊!欢迎来到冒险家协会。
再见。

View File

@ -0,0 +1,22 @@
robotName: 啊准
stages:
- name: start
answer: 我是啊准,一个可以查询天气的机器人。
transformers:
- pattern: .*?天气|气温.*?
nextStageName: weather
- pattern: .*?
nextStageName: running
- name: running
answer: 对不起,做不到。
transformers:
- pattern:
nextStageName: running
- name: weather
answer: 今天北京市的天气是@weather/text气温是@weather/temp 摄氏度。
transformers:
- pattern:
nextStageName: running
beginStageName: start

View File

@ -0,0 +1,2 @@
今天北京的天气怎么样?
你说的对,但是

View File

@ -0,0 +1,6 @@
我是啊准,一个可以查询天气的机器人。
今天北京市的天气是晴气温是20 摄氏度。
我是啊准,一个可以查询天气的机器人。
对不起,做不到。
我是啊准,一个可以查询天气的机器人。
再见。

View File

@ -0,0 +1,22 @@
robotName: 啊准
stages:
- name: start
answer: 我是啊准,一个可以查询天气的机器人。
transformers:
- pattern: .*?天气|气温.*?
nextStageName: weather
- pattern: .*?
nextStageName: running
- name: running
answer: 对不起,做不到。
transformers:
- pattern:
nextStageName: start
- name: weather
answer: 今天北京市的天气是@weather/text气温是@weather/temp 摄氏度。
transformers:
- pattern:
nextStageName: start
beginStageName: start

View File

@ -0,0 +1,2 @@
今天北京的天气怎么样?
你说的对,但是

View File

@ -0,0 +1,6 @@
我是啊准,一个可以查询天气的机器人。
今天北京市的天气是晴气温是20 摄氏度。
我是啊准,一个可以查询天气的机器人。
对不起,做不到。
我是啊准,一个可以查询天气的机器人。
再见。

View File

@ -0,0 +1,177 @@
using Katheryne.Abstractions;
using Katheryne.Exceptions;
using Katheryne.Modules;
using Katheryne.Services;
using Katheryne.Tests.Mocks;
using Microsoft.Extensions.Logging;
using Moq;
namespace Katheryne.Tests.Katheryne;
public class KatheryneRobotTests
{
private readonly Mock<ILogger<DefaultChatRobot>> _defaultChatRobotLogger = new();
private readonly Mock<ILogger<KatheryneChatRobot>> _katheryneChatRobotLogger = new();
private readonly Mock<ILogger<KatheryneChatRobotFactory>> _katheryneChatRobotFactoryLogger = new();
private readonly DefaultChatRobot _defaultChatRobot;
private readonly KatheryneChatRobotFactory _katheryneChatRobotFactory;
public KatheryneRobotTests()
{
_defaultChatRobot = new DefaultChatRobot(_defaultChatRobotLogger.Object);
_katheryneChatRobotFactory = new KatheryneChatRobotFactory(new YamlDeserializerFactory(),
_katheryneChatRobotFactoryLogger.Object,
_katheryneChatRobotLogger.Object,
_defaultChatRobot);
}
[Fact]
public void DefaultRobotTest()
{
InputOutputFile file = new("DefaultRobot");
ValidateOutput(_defaultChatRobot, file);
}
[Fact]
public void FactoryDefaultRobotTest()
{
InputOutputFile file = new("DefaultRobot");
ValidateOutput(_katheryneChatRobotFactory.GetRobot(), file);
}
[Fact]
public void KatheryneRobotTest1()
{
InputOutputFile file = new("Grammar1");
StreamReader reader = new(Path.Combine(file.PrefixPath, "grammar.yaml"));
_katheryneChatRobotFactory.SetGrammar(reader.ReadToEnd());
ValidateOutput(_katheryneChatRobotFactory.GetRobot(), file);
}
[Fact]
public void KatheryneRobotTest2()
{
InputOutputFile file = new("Grammar2");
StreamReader reader = new(Path.Combine(file.PrefixPath, "grammar.yaml"));
_katheryneChatRobotFactory.SetGrammar(reader.ReadToEnd());
ValidateOutput(_katheryneChatRobotFactory.GetRobot(), file);
}
[Fact]
public void RecursivelyExceptionTest()
{
IParamsModule weatherModule = new MockWeatherModule();
_katheryneChatRobotFactory.Modules.Clear();
_katheryneChatRobotFactory.Modules.Add(weatherModule.ModuleName, weatherModule);
InputOutputFile file = new("Grammar3");
StreamReader reader = new(Path.Combine(file.PrefixPath, "grammar.yaml"));
_katheryneChatRobotFactory.SetGrammar(reader.ReadToEnd());
Assert.Throws<GrammarException>(
() => ValidateOutput(_katheryneChatRobotFactory.GetRobot(), file));
}
[Fact]
public void KatheryneRobotTest4()
{
IParamsModule weatherModule = new MockWeatherModule();
_katheryneChatRobotFactory.Modules.Clear();
_katheryneChatRobotFactory.Modules.Add(weatherModule.ModuleName, weatherModule);
InputOutputFile file = new("Grammar4");
StreamReader reader = new(Path.Combine(file.PrefixPath, "grammar.yaml"));
_katheryneChatRobotFactory.SetGrammar(reader.ReadToEnd());
ValidateOutput(_katheryneChatRobotFactory.GetRobot(), file);
}
[Fact]
public void WeatherModuleTest()
{
const string grammar =
"""
robotName:
stages:
- name: running
answer:
transformers:
- pattern: .*?|.*?
nextStageName: weather
- pattern: .*?
nextStageName: running
- name: weather
answer: @weather/text@weather/temp
transformers:
- pattern:
nextStageName: running
beginStageName: running
""";
ModuleBase weatherModule = new WeatherModule();
_katheryneChatRobotFactory.Modules.Clear();
_katheryneChatRobotFactory.Modules.Add(weatherModule.ModuleName, weatherModule);
_katheryneChatRobotFactory.SetGrammar(grammar);
IChatRobot robot = _katheryneChatRobotFactory.GetRobot();
IEnumerable<string> answers = robot.ChatNext("今天天气怎么样?");
Assert.Contains(answers, answer =>
answer == $"今天璃月港的天气是{weatherModule["text"]},气温是{weatherModule["temp"]}。");
answers = robot.ChatNext("今天气温是多少度?");
Assert.Contains(answers, answer =>
answer == $"今天璃月港的天气是{weatherModule["text"]},气温是{weatherModule["temp"]}。");
}
private static void ValidateOutput(IChatRobot robot, InputOutputFile file)
{
foreach (string output in robot.OnChatStart())
{
string? except = file.Output.ReadLine();
Assert.NotNull(except);
Assert.Equal(except, output);
}
while (file.Input.Peek() >= 0)
{
string? input = file.Input.ReadLine();
Assert.NotNull(input);
foreach (string output in robot.ChatNext(input))
{
string? except = file.Output.ReadLine();
Assert.NotNull(except);
Assert.Equal(except, output);
}
}
foreach (string output in robot.OnChatStop())
{
string? except = file.Output.ReadLine();
Assert.NotNull(except);
Assert.Equal(except, output);
}
}
private class InputOutputFile
{
public StreamReader Input { get; }
public StreamReader Output { get; }
public string PrefixPath { get; }
public InputOutputFile(string testName)
{
PrefixPath = Path.Combine(Environment.CurrentDirectory, testName);
Input = new StreamReader(Path.Combine(PrefixPath, "in.txt"));
Output = new StreamReader(Path.Combine(PrefixPath, "out.txt"));
}
}
}

View File

@ -0,0 +1,21 @@
using Katheryne.Abstractions;
namespace Katheryne.Tests.Mocks;
public class MockWeatherModule : IParamsModule
{
private readonly Dictionary<string, string> _param = new()
{
{ "temp", "20" },
{ "text", "晴" }
};
public string ModuleName => "weather";
public string this[string param] => _param[param];
public bool ContainsParam(string param)
{
return _param.ContainsKey(param);
}
}

View File

@ -5,11 +5,27 @@ namespace Katheryne.Abstractions;
/// </summary>
public interface IChatRobot
{
/// <summary>
/// 定义机器人的名称
/// </summary>
public string RobotName { get; }
/// <summary>
/// 机器人在启动时输出的对话
/// </summary>
/// <returns>对话列表</returns>
public IEnumerable<string> OnChatStart();
/// <summary>
/// 机器人在结束对话时输出的对话列表
/// </summary>
/// <returns>对话列表</returns>
public IEnumerable<string> OnChatStop();
/// <summary>
/// 机器人在获得用户输入时输出的对话列表
/// </summary>
/// <param name="input">用户的输入</param>
/// <returns>机器人输出的对话列表</returns>
public IEnumerable<string> ChatNext(string input);
}

View File

@ -3,7 +3,7 @@ using Katheryne.Exceptions;
namespace Katheryne.Abstractions;
/// <summary>
/// 聊天机器人接口
/// 聊天机器人工厂接口
/// </summary>
public interface IChatRobotFactory
{

View File

@ -5,9 +5,21 @@ namespace Katheryne.Abstractions;
/// </summary>
public interface IParamsModule
{
/// <summary>
/// 模块的名称
/// </summary>
public string ModuleName { get; }
/// <summary>
/// 获得模块中特定指定参数的文本
/// </summary>
/// <param name="param">指定参数</param>
public string this[string param] { get; }
/// <summary>
/// 判断模块是否提供指定参数
/// </summary>
/// <param name="param">指定参数</param>
/// <returns>如果为真,说明模块提供该参数,反之没有提供</returns>
public bool ContainsParam(string param);
}

View File

@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using Katheryne.Abstractions;
using Katheryne.Exceptions;
using Katheryne.Models;
using Microsoft.Extensions.Logging;
@ -83,6 +84,10 @@ public class KatheryneChatRobot : IChatRobot
/// <param name="result">存放输出回答的列表</param>
private void EmptyTransform(List<string> result)
{
// 经过的状态集合, 避免递归循环
HashSet<string> movedStages = new();
movedStages.Add(_currentStage);
var flag = true;
while (flag)
{
@ -93,6 +98,16 @@ public class KatheryneChatRobot : IChatRobot
{
flag = true;
_currentStage = transformer.NextStage;
if (movedStages.Contains(_currentStage))
{
// 发生递归调用
throw new GrammarException("Recursively transform detected!");
}
else
{
movedStages.Add(_currentStage);
}
result.Add(_grammarTree[_currentStage].Answer.RowString);
_logger.LogDebug("Moving to stage {} with empty transform.",

View File

@ -1,3 +1,8 @@
namespace Katheryne.Models;
/// <summary>
/// 格式化标记
/// </summary>
/// <param name="Value">原始字符串</param>
/// <param name="Index">在匹配结果中的序号</param>
public record FormatTag(string Value, int Index);

View File

@ -1,3 +1,9 @@
namespace Katheryne.Models;
/// <summary>
/// 语法中的模块参数
/// </summary>
/// <param name="OriginString">原始字符串</param>
/// <param name="Module">模块名称</param>
/// <param name="Param">参数</param>
public record GrammarParam(string OriginString, string Module, string Param);

View File

@ -3,6 +3,9 @@ using Katheryne.Exceptions;
namespace Katheryne.Models;
/// <summary>
/// 语法树
/// </summary>
public class GrammarTree
{
private readonly Dictionary<string, InnerStage> _stages = new();

View File

@ -1,5 +1,8 @@
namespace Katheryne.Models;
/// <summary>
/// 词法模型
/// </summary>
public class LexicalModel
{
public required string RobotName { get; set; }

View File

@ -27,10 +27,21 @@ public class StringFormatter
GetFormatTags();
}
/// <summary>
/// 字符串是否需要进行格式化
/// </summary>
public bool IsFormat => _formatTags.Count != 0 || _params.Count != 0;
/// <summary>
/// 原始字符串
/// </summary>
public string RowString => _originString;
/// <summary>
/// 格式化字符串
/// </summary>
/// <param name="collection">正则表达式匹配的结果列表</param>
/// <returns>格式化之后的字符串</returns>
public string Format(GroupCollection collection)
{
var result = new string(_originString);
@ -54,6 +65,10 @@ public class StringFormatter
return result;
}
/// <summary>
/// 解析字符串中需要格式化的标签
/// </summary>
/// <exception cref="GrammarException">文法中调用的模块不存在</exception>
private void GetFormatTags()
{
List<int> tagIndices = new();

View File

@ -3,6 +3,9 @@ using Katheryne.Abstractions;
namespace Katheryne.Modules;
/// <summary>
/// 参数模块实现基类
/// </summary>
public abstract class ModuleBase : IParamsModule
{
protected readonly Dictionary<string, Func<string>> Functions = new();

View File

@ -4,6 +4,9 @@ using Katheryne.Exceptions;
namespace Katheryne.Modules;
/// <summary>
/// 提供天气服务的模块
/// </summary>
public class WeatherModule : ModuleBase
{
private static readonly HttpClient s_httpClient = new();

View File

@ -8,6 +8,9 @@ namespace Katheryne;
public static class ServiceCollectionExtensions
{
/// <summary>
/// 在服务集合中添加Katheryne DSL解释机器人服务
/// </summary>
public static void AddKatheryne(this IServiceCollection collection)
{
collection.AddSingleton<YamlDeserializerFactory>();

View File

@ -6,6 +6,9 @@ using YamlDotNet.Serialization;
namespace Katheryne.Services;
/// <summary>
/// Katheryne 聊天机器人工厂实现
/// </summary>
public class KatheryneChatRobotFactory : IChatRobotFactory
{
private readonly YamlDeserializerFactory _deserializerFactory;

View File

@ -3,6 +3,9 @@ using YamlDotNet.Serialization.NamingConventions;
namespace Katheryne.Services;
/// <summary>
/// YAML 反序列化对象工厂
/// </summary>
public class YamlDeserializerFactory
{
private readonly DeserializerBuilder _builder;

221
README.md Normal file
View File

@ -0,0 +1,221 @@
# 基于领域特定语言的客服机器人设计与实现
## 启动
安装[.NET SDK](https://dotnet.microsoft.com/en-us/download)。克隆仓库,进入`Frontend`文件夹启动项目。
```shell
git clone https://github.com/jackfiled/Katheryne.git
cd Frontend
dotnet run
```
## Gallery
![image-20240919211237542](./README/image-20240919211237542.png)
<img src="./README/image-20240919211341189.png" alt="image-20240919211341189" style="zoom:50%;" />
![image-20240919211529669](./README/image-20240919211529669.png)
## 需求分析
项目要求实现一个面向客服机器人领域的领域特定语言DSL解释器。
其中**领域特定语言**是一种相对简单的文法,用于在特定领域的业务流程定制。在本次项目实现中,要求定义一个描述在线客服机器人自动应答逻辑的领域特定语言,即:
- 支持根据用户不同的输入返回不同的输出
- 调用指定的接口获得信息返回给用户
- 从用户输入的文本中提取信息进行返回
## 领域特定语言文法说明
基于上述对于领域特定语言的要求,设计如下的领域特定文法。
文法中的核心是定义**状态**,每个状态对应了机器人的一句输出,在每个状态中定义了**迁移关系**,通过**正则表达式**匹配用户的输入来迁移到不同的状态。因此该文法的定义的机器人是用户输入驱动的,只有当用户进行输入时机器人才会给出对应的输出。
领域特定文法采用类似于`YAML`语言的语法进行定义。
文法文本拥有三个顶级属性:
- `robotName` 字符串类型,规定了机器人的名称;
- `stages` Stage类型的数组规定了机器人的各个阶段
- `beginStageName` 字符串类型,规定了机器人初始阶段,会自动输出该阶段的输出内容。
Stage类型拥有三个属性
- `name` 阶段的名称,是阶段**唯一的标识符**
- `transformers` Transformer类型的数组指定该阶段的迁移规则
- `answer` 该阶段的输出内容。
Transformer类型拥有两个属性
- `pattern` 匹配用户输入的正则表达式;
- `nextStageName` 匹配成功之后需要迁移到的阶段名。
参照下面的例子:
```yaml
robotName: 移动客服
stages:
- name: run
answer: 欢迎致电中国移动10086客服热线
transfomrers:
- pattern: .*?
nextStageName: run
beginStageName: run
```
在上述的文法中定义了一个最简单的客服机器人,这个机器人的名称为“移动客服”,机器人有且只有一个状态`run`就是输出“欢迎致电中国移动10086客服热线”而且`.*?`的正则表达式在表示不论用户输入任何内容的文本,机器人都会再次迁移到`run`状态而继续输出“欢迎致电中国移动10086客服热线”。机器人在启动时也处于`run`状态因此在首次运行时机器人也会输出“欢迎致电中国移动10086客服热线”。
在`Tranform`类型的`pattern`属性支持一种特殊的空匹配模式,即当`pattern`属性留空时,机器人会在不等待用户输入的情况下直接迁移到下一个状态,扩展了文法的表现力。
在`Stage`类型的`answer`属性中支持两种特殊的扩展语法:
- 利用上文中设置的迁移正则表达式中提取用户的输入作为输出的一部分,使用`$number` 作为标识符指定提取到的信息应当插入在何处,`number`是匹配成功的列表索引。
例如,使用正则表达式`(.*?)`迁移到拥有如下 `answer: 感谢冒险家完成了“$1”, 这是你的奖励。`属性的阶段, 会将用户的输入完全替换到`$1`所在的位置。当用户输入“攀高危险”文本时,机器人就会输出“感谢冒险家完成了‘攀高危险’,这是你的奖励”。
- 调用系统中预先定义的API作为回答使用`@module/method`调用。
例如:利用`@weather/text`调用weather模块的text方法获得当前北京的天气信息。对于`answer: 今天北京的天气是@wether/text`的阶段来说,假定当前模块返回的北京天气为“晴”,机器人的输出就会是“今天北京的天气是晴”。
当前程序中提供的API模块有
- Weather:
text: 获得当前天气文本
temp: 获得当前温度
为了保证用户输入的文法正确性,在编译用户输入的文法时会进行按照进行下面列出的检查:
- `transformers` 中的 `nextStageName`指定的阶段是否定义;
- `beginStageName` 指定的阶段是否定义。
- 调用的模块和方法是否存在。
## 项目框架简介
### 核心解释器
项目中核心解释器使用[.NET](https://dotnet.microsoft.com/zh-cn/)平台的[C#](https://learn.microsoft.com/zh-cn/dotnet/csharp/)语言进行实现。
.NET是一个免费的跨平台的开源开发人员平台可以使用多种语言、编辑器和库连构建Web移动、桌面、游戏等应用。C#语言是一种简单、现代、面向对象和类型安全的编程语言。
### 人机接口界面
项目中的人机接口界面采用基于[ASP.NET](https://learn.microsoft.com/zh-cn/aspnet/core/?view=aspnetcore-8.0)中的[Blazor](https://learn.microsoft.com/zh-cn/aspnet/core/blazor/?view=aspnetcore-8.0)技术开发的网页界面提供。
Blazor是一种.NET前端框架在单个编程模型中同时支持服务器端呈现和客户端交互性。
## 程序模块划分和接口设计
### 程序模块划分
程序在整体上分为三个模块,通过解决方案中的三个不同项目表现:
- 核心接口设计和解释器实现:`Katheryne`项目
- 解释器的测试:`Katheryne.Tests`项目
- 人机接口界面实现:`Frontene`项目
其中后面两个项目依赖第一个项目。
![image-20231126114723300](./README/image-20231126114723300.png)
### 程序接口设计
为了是解释器实现和测试、人机接口分离,在`Katheryne.Abstractions`命名空间中定义了对话机器人的抽象接口,也即脚本语言解释器对外提供的功能。
> 在C#语言中,命名空间和文件夹相对应,因此命名空间`Katheryne.Abstractions`就在`Katheryne`项目的`Abstractions`文件夹下实现。
#### 聊天机器人接口
聊天机器人接口是对同用户对话的机器人的抽象,在接口中定义了:
- 聊天机器人名称
- 机器人在启动时输出的对话
- 机器人在结束对话时输出的对话
- 机器人在获得特定用户输入时输出的对话
#### 聊天机器人工厂接口
按照工厂模式的设计原则,设计聊天机器人工厂,通过设置文法来“生产”按照指定的文法运行的机器人。
聊天机器人工厂接口定义了:
- 获得当前使用的语法文本
- 设置机器人需要使用的文法
- 创造按照当前文法运行的机器人,即实现了上述聊天机器人接口的对象
#### 模块接口
鉴于在文法中提供了通过模块名称和参数名称调用模块的方法,制定模块接口以提供文法设计者提供新的模块之功能。
模块接口定义了:
- 获得模块的名称
- 判断模块是否提供指定的参数
- 获得指定参数对应的结果
## 解释器核心数据结构和实现细节
解释器中的核心数据结构是语法树`GrammarTree`。在语法树中的核心数据结构是以阶段名为键,阶段对象为值的哈希表。当通过转换关系获得下一个阶段的名称,从哈希表中可以快速获得下一个阶段对象并格式化输出。
在进行格式化输出时,需要进行两个格式化操作:
- 从用户的输入匹配的正则表达式中提取信息
- 调用模块获得信息。
因此,设计`StringFormatter`类支持上述两个操作。在构建`StringFormatter`类的同时检查文法中调用的模块及其参数是否存在,并将文法中需要从用户的输入中提取信息的序号和标签保存在列表中,需要调用模块中获得信息的模块名、参数名和标签也保存在列表中。在进行格式化操作时直接遍历列表进行格式化操作,提高各司花的效率。
在机器人运行过程中可能由于错误的脚本设计或者错误的用户输入而导致运行时异常。例如:
- 在脚本中利用空转移语法错误的编写了死循环
- 在脚本中提取用户输入时指定的序号导致数组越界
机器人程序会自动捕获这些异常并封装抛出一个统一的错误`GrammarException`,方便外部接口进行进一步的错误处理。
## 测试
采用`XUnit`作为测试框架编写测试样例。
在`Katheryne.Tests`项目中设计两种测试样例:
- 针对项目中某一部分代码进行的单元测试
- 指定文法和输入输出对解释器进行测试的集成测试
同时基于自建的`Gitea Actions`系统设置了在每次`git`推送提交之后自动运行测试的系统。
![image-20231125185207998](./README/image-20231125185207998.png)
### 单元测试
在程序中设计了如下单元测试:
1. 词法分析测试。
通过给出脚本,判断程序是否能够正确将解析脚本中的各种元素。
2. 输出格式化测试。
鉴于在语法设计过程中设计了大量输出的扩展语法,输出部分代码逻辑较为复杂,故单独设计测试模块。
故针对从用户输入提取信息和调用内部模块两种扩展语法设置单元测试,判断程序能够正确格式化输出和是否判断文法中对模块的调用合法。
3. 模块测试
为了保证程序中提供的模块工作正常,对程序中提供的每一个模块单独编写测试用例。
### 集成测试
通过编写DSL脚本文件`grammar.yaml`、机器人的输入文件`in.txt`和机器人的输出文件`out.txt`。程序自动构造使用该脚本文件的机器人,从输入文件中读取内容作为机器人的输入,将机器人的输出同输出文件做比较,进行机器人的整体集成测试。测试也分成正常测试和异常测试两种,正常测试判断机器人是否正常进行工作,异常测试判断机器人能否在异常脚本/异常输入中正确抛出异常。
### 测试覆盖率
使用工具获得单元测试的覆盖率为:
![image-20231127123547234](./README/image-20231127123547234.png)
## 结论和心得
在本次大作业中设计了一门面向客服机器人领域的领域特定脚本语言并实现了该领域特定脚本语言的解释器。
通过这次大作业,我实践了在《程序设计实践》课程上学习到的各种程序设计实践方式,尝试从软件开发的完整过程审视程序设计中需要注意的各种问题,力图写出运行正确,可读性稿,设计优雅的跨平台高性能程序。同时在编写程序中的过程中尝试各种现代工具辅助程序的开发,例如结合流行的`DevOps`概念在`Git`平台上部署`CI/CD`流程,使用`docker`作为程序的分发平台。
在完成这次大作业之后,我认识到了在程序设计过程中需要注意的各种问题,从顶层的程序接口设计到底层每句代码的编写和注释的编写,通过各种代码设计规范和编写工具的辅助,可以更高效的编写简介而高性能的代码。同时,各种单元测试和集成测试的编写也辅助我发现了程序中存在的各种问题,表明了测试的重要性。

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB