train model by input contexts.

This commit is contained in:
haiping008@gmail.com 2018-06-18 17:15:32 -05:00
parent 220b6ad209
commit ad309dc68d
13 changed files with 147 additions and 212 deletions

View file

@ -93,6 +93,7 @@ namespace BotSharp.Core.Agents
{
Intent = intent.Name,
Text = String.Join("", exp.Data.OrderBy(x => x.UpdatedTime).Select(x => x.Text)),
ContextHash = intent.ContextHash
};
// convert entity format
@ -137,6 +138,9 @@ namespace BotSharp.Core.Agents
});
});
// remove Default Fallback Intent
trainingData.UserSays = trainingData.UserSays.Where(x => x.Intent != "Default Fallback Intent").ToList();
return trainingData;
}
}

View file

@ -23,6 +23,10 @@
<PackageProjectUrl>https://github.com/Oceania2018/BotSharp</PackageProjectUrl>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>TRACE;DEBUG;MODEL_PER_CONTEXTS</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetToolkit" Version="1.4.0" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="1.6.0" />

View file

@ -108,6 +108,12 @@ namespace BotSharp.Core.Engines
{
string expressionJson = File.ReadAllText($"{expressionFileName}");
intent.UserSays = JsonConvert.DeserializeObject<List<DialogflowIntentExpression>>(expressionJson);
// remove @sys.ignore
intent.UserSays.ForEach(say =>
{
say.Data.Where(x => x.Meta == "@sys.ignore").ToList().ForEach(x => x.Meta = null);
});
}
}

View file

@ -2,6 +2,7 @@
using BotSharp.Core.Entities;
using BotSharp.Core.Intents;
using BotSharp.Core.Models;
using DotNetToolkit;
using EntityFrameworkCore.BootKit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@ -49,9 +50,6 @@ namespace BotSharp.Core.Engines
var corpus = agent.GrabCorpus(dc);
// remove Default Fallback Intent
corpus.UserSays = corpus.UserSays.Where(x => x.Intent != "Default Fallback Intent").ToList();
string json = JsonConvert.SerializeObject(new { rasa_nlu_data = corpus },
new JsonSerializerSettings
{
@ -59,14 +57,10 @@ namespace BotSharp.Core.Engines
NullValueHandling = NullValueHandling.Ignore
});
#if RASA_NLU_0_11
rest.AddParameter("application/json", json, ParameterType.RequestBody);
#else
string trainingConfig = agent.Language == "zh" ? "config_jieba_mitie_sklearn.yml" : "config_mitie_sklearn.yml";
string trainingConfig = agent.Language == "zh" ? "config_jieba_mitie_sklearn.yml" : "config_spacy.yml";
string body = File.ReadAllText($"{Database.ContentRootPath}{Path.DirectorySeparatorChar}Settings{Path.DirectorySeparatorChar}{trainingConfig}");
body = $"{body}\r\ndata: {json}";
rest.AddParameter("application/x-yml", body, ParameterType.RequestBody);
#endif
var response = client.Execute(rest);
@ -88,47 +82,93 @@ namespace BotSharp.Core.Engines
}
}
public string TrainWithContexts()
public void TrainWithContexts()
{
var corpus = agent.GrabCorpus(dc);
string json = JsonConvert.SerializeObject(new { rasa_nlu_data = corpus },
new JsonSerializerSettings
var client = new RestClient($"{Database.Configuration.GetSection("Rasa:Nlu").Value}");
var contextHashs = corpus.UserSays
.Select(x => x.ContextHash)
.Distinct()
.ToList();
contextHashs.ForEach(ctx =>
{
var data = new RasaTrainingData
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
Entities = corpus.Entities,
UserSays = corpus.UserSays.Where(x => x.ContextHash == ctx).ToList()
};
// meet minimal requirement
// at least 2 different classes
int count = data.UserSays
.Select(x => x.Intent)
.Distinct().Count();
if (count < 2)
{
data.UserSays.Add(new RasaIntentExpression
{
Intent = "Intent2",
Text = Guid.NewGuid().ToString("N")
});
data.UserSays.Add(new RasaIntentExpression
{
Intent = "Intent2",
Text = Guid.NewGuid().ToString("N")
});
}
// at least 2 corpus per intent
data.UserSays.Select(x => x.Intent)
.Distinct()
.ToList()
.ForEach(intent =>
{
if(data.UserSays.Count(x => x.Intent == intent) < 2)
{
data.UserSays.Add(new RasaIntentExpression
{
Intent = intent,
Text = Guid.NewGuid().ToString("N")
});
}
});
var client = new RestClient($"{Database.Configuration.GetSection("Rasa:Host").Value}");
var rest = new RestRequest("train", Method.POST);
rest.AddQueryParameter("project", agent.Id);
rest.AddParameter("application/json", json, ParameterType.RequestBody);
string json = JsonConvert.SerializeObject(new { rasa_nlu_data = data },
new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
});
var response = client.Execute(rest);
var rest = new RestRequest("train", Method.POST);
rest.AddQueryParameter("project", agent.Id);
rest.AddQueryParameter("model", ctx);
string trainingConfig = agent.Language == "zh" ? "config_jieba_mitie_sklearn.yml" : "config_spacy.yml";
string body = File.ReadAllText($"{Database.ContentRootPath}{Path.DirectorySeparatorChar}Settings{Path.DirectorySeparatorChar}{trainingConfig}");
body = $"{body}\r\ndata: {json}";
rest.AddParameter("application/x-yml", body, ParameterType.RequestBody);
if (response.IsSuccessful)
{
var result = JObject.Parse(response.Content);
var response = client.Execute(rest);
string modelName = result["info"].Value<String>().Split(": ")[1];
dc.Table<ContextModelMapping>().Add(new ContextModelMapping
if (response.IsSuccessful)
{
AgentId = agent.Id,
ModelName = modelName,
//ContextId = contextId
});
var result = JObject.Parse(response.Content);
return modelName;
}
else
{
var result = JObject.Parse(response.Content);
string modelName = result["info"].Value<String>().Split(": ")[1];
}
else
{
var result = JObject.Parse(response.Content);
Console.WriteLine(result["error"]);
result["error"].Log();
}
});
Console.WriteLine(result["error"]);
return String.Empty;
}
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
@ -12,7 +13,10 @@ namespace BotSharp.Core.Models
public String Text { get; set; }
public String Intent { get; set; }
[JsonIgnore]
public String ContextHash { get; set; }
public List<RasaIntentExpressionPart> Entities { get; set; }
}
}

View file

@ -7,6 +7,7 @@ namespace BotSharp.Core.Adapters.Rasa
{
public class RasaTraningEntity
{
[JsonIgnore]
public String EntityType { get; set; }
[JsonProperty("value")]

View file

@ -30,7 +30,12 @@ namespace BotSharp.Core.Engines
AIResponse aiResponse = new AIResponse();
Database dc = rasa.dc;
#if MODEL_PER_CONTEXTS
string model = GetModelPerContexts(rasa, request);
var result = CallRasa(rasa.agent.Id, request.Query.First(), model);
#else
var result = CallRasa(rasa.agent.Id, request.Query.First(), rasa.agent.Id);
#endif
RasaResponse response = result.Data;
aiResponse.Id = Guid.NewGuid().ToString();
aiResponse.Lang = rasa.agent.Language;
@ -114,7 +119,7 @@ namespace BotSharp.Core.Engines
};
}
response.IntentRanking = response.IntentRanking.Where(x => x.Confidence > decimal.Parse("0.1")).ToList();
response.IntentRanking = response.IntentRanking.Where(x => x.Confidence > decimal.Parse("0.3")).ToList();
response.IntentRanking = response.IntentRanking.Where(x => intents.Select(i => i.Name).Contains(x.Name)).ToList();
// add Default Fallback Intent
@ -165,10 +170,14 @@ namespace BotSharp.Core.Engines
// convert to Standard entity value
if (!String.IsNullOrEmpty(p.Value) && !p.DataType.StartsWith("@sys."))
{
p.Value = agent.Entities.FirstOrDefault(x => x.Name == p.Name).Entries.FirstOrDefault((entry) => {
return entry.Value.ToLower() == p.Value.ToLower() ||
entry.Synonyms.Select(synonym => synonym.Synonym.ToLower()).Contains(p.Value.ToLower());
})?.Value;
p.Value = agent.Entities
.FirstOrDefault(x => x.Name == p.DataType.Substring(1))
.Entries
.FirstOrDefault((entry) =>
{
return entry.Value.ToLower() == p.Value.ToLower() ||
entry.Synonyms.Select(synonym => synonym.Synonym.ToLower()).Contains(p.Value.ToLower());
})?.Value;
}
// fixed entity per request
@ -202,11 +211,14 @@ namespace BotSharp.Core.Engines
}
else
{
msg.Speech = msg.Speech.StartsWith("[") ?
if (msg.Speech != "[]")
{
msg.Speech = msg.Speech.StartsWith("[") ?
ArrayHelper.GetRandom(msg.Speech.Substring(2, msg.Speech.Length - 4).Split("\",\"").ToList()) :
msg.Speech;
msg.Speech = ReplaceParameters4Response(intentResponse.Parameters, msg.Speech);
msg.Speech = ReplaceParameters4Response(intentResponse.Parameters, msg.Speech);
}
}
});
}
@ -285,10 +297,8 @@ namespace BotSharp.Core.Engines
return client.Execute<RasaResponse>(rest);
}
public static AIResponse TextRequestPerContexts(this RasaAi rasa, AIRequest request)
private static string GetModelPerContexts(RasaAi rasa, AIRequest request)
{
AIResponse aiResponse = new AIResponse();
RasaResponse response = null;
Database dc = rasa.dc;
// Merge input contexts
@ -316,138 +326,9 @@ namespace BotSharp.Core.Engines
}).OrderByDescending(x => x.Contexts.Count).ToList();
// query per request contexts
{
string contextId = $"{String.Join(',', contexts.Select(x => x.Name))}".GetMd5Hash();
string modelName = dc.Table<ContextModelMapping>().FirstOrDefault(x => x.ContextId == contextId)?.ModelName;
var result = CallRasa(rasa.agent.Id, request.Query.First(), modelName);
var contextHashs = intents.Select(x => x.ContextHash).Distinct().ToList();
if (result.Data.Intent != null)
{
response = result.Data;
}
}
// Max contexts match
if (response == null)
{
foreach (var it in intents)
{
request.Contexts = it.Contexts.Select(x => new AIContext { Name = x.Name.ToLower() })
.OrderBy(x => x.Name)
.ToList();
string contextId = $"{String.Join(',', request.Contexts.Select(x => x.Name))}".GetMd5Hash();
string modelName = dc.Table<ContextModelMapping>().FirstOrDefault(x => x.ContextId == contextId)?.ModelName;
var result = CallRasa(rasa.agent.Id, request.Query.First(), modelName);
if (result.Data.Intent != null)
{
response = result.Data;
break;
}
};
}
var intent = (dc.Table<Intent>().Where(x => x.Name == response.Intent.Name)
.Include(x => x.Responses).ThenInclude(x => x.Contexts)
.Include(x => x.Responses).ThenInclude(x => x.Parameters)
.Include(x => x.Responses).ThenInclude(x => x.Messages)).First();
var intentResponse = ArrayHelper.GetRandom(intent.Responses);
aiResponse.Id = Guid.NewGuid().ToString();
aiResponse.Lang = rasa.agent.Language;
aiResponse.Status = new AIResponseStatus { };
aiResponse.SessionId = rasa.AiConfig.SessionId;
aiResponse.Timestamp = DateTime.UtcNow;
intentResponse.Messages = intentResponse.Messages.OrderBy(x => x.UpdatedTime).ToList();
intentResponse.Messages.ToList()
.ForEach(msg =>
{
if (msg.Type == AIResponseMessageType.Custom)
{
}
else
{
msg.Speech = msg.Speech.StartsWith("[") ?
ArrayHelper.GetRandom(msg.Speech.Substring(2, msg.Speech.Length - 4).Split("\",\"").ToList()) :
msg.Speech;
}
});
aiResponse.Result = new AIResponseResult
{
Source = "agent",
ResolvedQuery = request.Query.First(),
Action = intentResponse.Action,
Parameters = new Dictionary<string, string>(),
Score = response.Intent.Confidence,
Metadata = new AIResponseMetadata { IntentId = intent.Id, IntentName = intent.Name },
Fulfillment = new AIResponseFulfillment
{
Messages = intentResponse.Messages.Select(x => {
if (x.Type == AIResponseMessageType.Custom)
{
return (new
{
x.Type,
x.Payload
}) as Object;
}
else
{
return (new { x.Type, x.Speech }) as Object;
}
}).ToList()
}
};
// Merge context lifespan
// override if exists, otherwise add, delete if lifespan is zero
dc.DbTran(() =>
{
var sessionContexts = dc.Table<ConversationContext>().Where(x => x.ConversationId == rasa.AiConfig.SessionId).ToList();
// minus 1 round
sessionContexts.Where(x => !intentResponse.Contexts.Select(ctx => ctx.Name).Contains(x.Context))
.ToList()
.ForEach(ctx => ctx.Lifespan = ctx.Lifespan - 1);
intentResponse.Contexts.ForEach(ctx =>
{
var session1 = sessionContexts.FirstOrDefault(x => x.Context == ctx.Name);
if (session1 != null)
{
if (ctx.Lifespan == 0)
{
dc.Table<ConversationContext>().Remove(session1);
}
else
{
session1.Lifespan = ctx.Lifespan;
}
}
else
{
dc.Table<ConversationContext>().Add(new ConversationContext
{
ConversationId = rasa.AiConfig.SessionId,
Context = ctx.Name,
Lifespan = ctx.Lifespan
});
}
});
});
aiResponse.Result.Contexts = dc.Table<ConversationContext>()
.Where(x => x.ConversationId == rasa.AiConfig.SessionId)
.Select(x => new AIContext { Name = x.Context.ToLower(), Lifespan = x.Lifespan })
.ToArray();
return aiResponse;
return contextHashs.FirstOrDefault();
}
}
}

View file

@ -1,25 +0,0 @@
using EntityFrameworkCore.BootKit;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
namespace BotSharp.Core.Intents
{
[Table("Bot_ContextModelMapping")]
public class ContextModelMapping : DbRecord, IDbRecord
{
[Required]
[StringLength(36)]
public String AgentId { get; set; }
[Required]
[StringLength(32)]
public string ContextId { get; set; }
[Required]
[StringLength(21)]
public string ModelName { get; set; }
}
}

View file

@ -36,7 +36,9 @@ namespace BotSharp.Core.Intents
{
get
{
return $"{String.Join(',', Contexts.OrderBy(x => x.Name).Select(x => x.Name))}".GetMd5Hash();
return Contexts == null || Contexts.Count == 0
? Guid.Empty.ToString("N")
: $"{String.Join(',', Contexts.OrderBy(x => x.Name).Select(x => x.Name))}".GetMd5Hash();
}
}

View file

@ -82,9 +82,7 @@ namespace BotSharp.UnitTest
var rasa = new RasaAi(dc, config);
string msg = rasa.TrainWithContexts();
Assert.IsTrue(!String.IsNullOrEmpty(msg));
rasa.TrainWithContexts();
}
}
}

View file

@ -7,11 +7,8 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="App_Data\DbInitializer\Agents\Dialogflow\VirtualAssistant\**" />
<Compile Remove="App_Data\DbInitializer\Agents\Dialogflow\Voicebot\**" />
<EmbeddedResource Remove="App_Data\DbInitializer\Agents\Dialogflow\VirtualAssistant\**" />
<EmbeddedResource Remove="App_Data\DbInitializer\Agents\Dialogflow\Voicebot\**" />
<None Remove="App_Data\DbInitializer\Agents\Dialogflow\VirtualAssistant\**" />
<None Remove="App_Data\DbInitializer\Agents\Dialogflow\Voicebot\**" />
</ItemGroup>

View file

@ -22,6 +22,29 @@ namespace BotSharp.UnitTest
// Round 1
var response = rasa.TextRequest(new AIRequest { Query = new String[] { "Can you play country music?" } });
Assert.AreEqual(response.Result.Metadata.IntentName, "music.play");
Assert.AreEqual(response.Result.Contexts.First(x => x.Name == "music-player-control").Lifespan, 3);
Assert.AreEqual(response.Result.Contexts.First(x => x.Name == "play-music").Lifespan, 5);
Assert.AreEqual(response.Result.Parameters.First(x => x.Key == "genre").Value, "country");
// Round 2
response = rasa.TextRequest(new AIRequest { Query = new String[] { "pause it" } });
Assert.AreEqual(response.Result.Metadata.IntentName, "music_player_control.pause");
Assert.AreEqual(response.Result.Contexts.First(x => x.Name == "music-player-control").Lifespan, 3);
Assert.AreEqual(response.Result.Contexts.First(x => x.Name == "play-music").Lifespan, 4);
// Round 3
response = rasa.TextRequest(new AIRequest { Query = new String[] { "continue" } });
Assert.AreEqual(response.Result.Metadata.IntentName, "music_player_control.resume");
Assert.AreEqual(response.Result.Contexts.First(x => x.Name == "music-player-control").Lifespan, 3);
Assert.AreEqual(response.Result.Contexts.First(x => x.Name == "play-music").Lifespan, 3);
// Round 4
response = rasa.TextRequest(new AIRequest { Query = new String[] { "play Hard Times by David Newman" } });
Assert.AreEqual(response.Result.Metadata.IntentName, "music.play");
Assert.AreEqual(response.Result.Contexts.First(x => x.Name == "music-player-control").Lifespan, 3);
Assert.AreEqual(response.Result.Contexts.First(x => x.Name == "play-music").Lifespan, 5);
Assert.AreEqual(response.Result.Parameters.First(x => x.Key == "song").Value, "Hard Times");
Assert.AreEqual(response.Result.Parameters.First(x => x.Key == "artist").Value, "David Newman");
}
}
}

View file

@ -1,6 +1,6 @@
{
"Rasa": {
"Nlu": "http://localhost:5000"
"Nlu": "http://gtx.local:5000"
},
"BotSharpAi": {