diff --git a/BotSharp.Channel.FacebookMessenger/BotSharp.Channel.FacebookMessenger.csproj b/BotSharp.Channel.FacebookMessenger/BotSharp.Channel.FacebookMessenger.csproj index 1add5556..56f689c2 100644 --- a/BotSharp.Channel.FacebookMessenger/BotSharp.Channel.FacebookMessenger.csproj +++ b/BotSharp.Channel.FacebookMessenger/BotSharp.Channel.FacebookMessenger.csproj @@ -14,6 +14,7 @@ + diff --git a/BotSharp.Channel.Weixin/BotSharp.Channel.Weixin.csproj b/BotSharp.Channel.Weixin/BotSharp.Channel.Weixin.csproj index d9bd907c..89f54ed6 100644 --- a/BotSharp.Channel.Weixin/BotSharp.Channel.Weixin.csproj +++ b/BotSharp.Channel.Weixin/BotSharp.Channel.Weixin.csproj @@ -25,6 +25,7 @@ + diff --git a/BotSharp.Platform.Rasa/AgentImporterInRasa.cs b/BotSharp.Platform.Rasa/AgentImporterInRasa.cs new file mode 100644 index 00000000..13c1739c --- /dev/null +++ b/BotSharp.Platform.Rasa/AgentImporterInRasa.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BotSharp.Platform.Abstraction; +using BotSharp.Platform.Models; +using BotSharp.Platform.Models.Intents; +using BotSharp.Platform.Rasa.Models; +using Newtonsoft.Json; + +namespace BotSharp.Platform.Rasa +{ + public class AgentImporterInRasa : IAgentImporter where TAgent : AgentModel, new() + { + public string AgentDir { get; set; } + + public async Task LoadAgent(AgentImportHeader agentHeader) + { + var agent = new TAgent + { + Id = agentHeader.Id, + Name = agentHeader.Name + }; + + return agent; + } + + public async Task LoadBuildinEntities(TAgent agent) + { + agent.Intents.ForEach(intent => + { + /*if (intent.UserSays != null) + { + intent.UserSays.ForEach(us => + { + us.Data.Where(data => data.Meta != null) + .ToList() + .ForEach(data => + { + LoadBuildinEntityTypePerUserSay(agent, data); + }); + }); + }*/ + }); + } + + private void LoadBuildinEntityTypePerUserSay(TAgent agent, IntentExpressionPart data) + { + /*var existedEntityType = agent.Entities.FirstOrDefault(x => x.Name == data.Meta); + + if (existedEntityType == null) + { + existedEntityType = new EntityType + { + Name = data.Meta, + Entries = new List(), + IsOverridable = true + }; + + agent.Entities.Add(existedEntityType); + } + + var entries = existedEntityType.Entries.Select(x => x.Value.ToLower()).ToList(); + if (!entries.Contains(data.Text.ToLower())) + { + existedEntityType.Entries.Add(new EntityEntry + { + Value = data.Text, + Synonyms = new List + { + new EntrySynonym + { + Synonym = data.Text + } + } + }); + }*/ + } + + public async Task LoadCustomEntities(TAgent agent) + { + } + + public async Task LoadIntents(TAgent agent) + { + string data = File.ReadAllText(Path.Combine(AgentDir, "corpus.json")); + var rasa = JsonConvert.DeserializeObject(data); + + agent.Intents = rasa.Data.Intents; + agent.Entities = rasa.Data.Entities; + } + + private void ImportIntentUserSays(RasaIntentExpression intent, List sentences) + { + var intents = new List(); + + var userSays = sentences.Where(x => x.Intent == intent.Intent).ToList(); + + userSays.ForEach(say => + { + var expression = new IntentExpression(); + + say.Entities = say.Entities.OrderBy(x => x.Start).ToList(); + + expression.Data = new List(); + + int pos = 0; + for (int entityIdx = 0; entityIdx < say.Entities.Count; entityIdx++) + { + var entity = say.Entities[entityIdx]; + + // previous + if (entity.Start > 0) + { + expression.Data.Add(new IntentExpressionPart + { + Text = say.Text.Substring(pos, entity.Start - pos), + Start = pos + }); + } + + // self + expression.Data.Add(new IntentExpressionPart + { + Alias = entity.Entity, + Meta = entity.Entity, + Text = say.Text.Substring(entity.Start, entity.Value.Length), + Start = entity.Start + }); + + pos = entity.End + 1; + + if (pos < say.Text.Length && entityIdx == say.Entities.Count - 1) + { + // end + expression.Data.Add(new IntentExpressionPart + { + Text = say.Text.Substring(pos), + Start = pos + }); + } + } + + if (say.Entities.Count == 0) + { + expression.Data.Add(new IntentExpressionPart + { + Text = say.Text.Substring(pos) + }); + } + + int second = 0; + expression.Data.ForEach(x => x.UpdatedTime = DateTime.UtcNow.AddSeconds(second++)); + + intents.Add(say); + }); + } + + /*public void AssembleTrainData(TAgent agent) + { + // convert agent to training corpus + agent.Corpus = new TrainingCorpus + { + Entities = new List(), + UserSays = new List>() + }; + + agent.Intents.ForEach(intent => + { + intent.UserSays.ForEach(say => { + agent.Corpus.UserSays.Add(new TrainingIntentExpression + { + Intent = intent.Name, + Text = String.Join("", say.Data.Select(x => x.Text)), + Entities = say.Data.Where(x => !String.IsNullOrEmpty(x.Meta)) + .Select(x => new TrainingIntentExpressionPart + { + Value = x.Text, + Entity = x.Meta, + Start = x.Start + }) + .ToList() + }); + }); + }); + }*/ + } +} diff --git a/BotSharp.Platform.Rasa/BotSharp.Platform.Rasa.csproj b/BotSharp.Platform.Rasa/BotSharp.Platform.Rasa.csproj new file mode 100644 index 00000000..cc5394f0 --- /dev/null +++ b/BotSharp.Platform.Rasa/BotSharp.Platform.Rasa.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp2.1 + + + + + + + + + + + diff --git a/BotSharp.Platform.Rasa/Controllers/ConfigController.cs b/BotSharp.Platform.Rasa/Controllers/ConfigController.cs new file mode 100644 index 00000000..b0a05188 --- /dev/null +++ b/BotSharp.Platform.Rasa/Controllers/ConfigController.cs @@ -0,0 +1,29 @@ +using BotSharp.Platform.Rasa.Models; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Controllers +{ + [Route("[controller]")] + public class ConfigController : ControllerBase + { + [HttpGet] + public ActionResult Get() + { + var status = new RasaStatusModel + { + AvailableProjects = JObject.FromObject(new RasaProjectModel + { + Status = "ready", + AvailableModels = new List { "model_XXXXXX" }, + LoadedModels = new List { "model_XXXXXX" } + }) + }; + + return Ok(status); + } + } +} diff --git a/BotSharp.Platform.Rasa/Controllers/ParseController.cs b/BotSharp.Platform.Rasa/Controllers/ParseController.cs new file mode 100644 index 00000000..8cb82cca --- /dev/null +++ b/BotSharp.Platform.Rasa/Controllers/ParseController.cs @@ -0,0 +1,98 @@ +using BotSharp.Platform.Rasa.Models; +using Microsoft.AspNetCore.Mvc; + +namespace BotSharp.Platform.Rasa.Controllers +{ + /// + /// send a text request + /// + [Route("[controller]")] + public class ParseController : ControllerBase + { + private readonly RasaAi builder; + + /// + /// Initialize dialog controller and get a platform instance + /// + /// + public ParseController(RasaAi configuration) + { + builder = configuration; + } + + /// + /// parse request + /// + /// + /// + [HttpPost, HttpGet] + public ActionResult Parse(RasaRequestModel request) + { + /*var config = new AIConfiguration("", SupportedLanguage.English); + config.SessionId = "rasa nlu"; + + string body = ""; + using (var reader = new StreamReader(Request.Body)) + { + body = reader.ReadToEnd(); + } + + Console.WriteLine($"Got message from {Request.Host}: {body}", Color.Green); + if(request.Project ==null && !String.IsNullOrEmpty(body)) + { + request = JsonConvert.DeserializeObject(body); + } + + // Load agent + var projectPath = Path.Combine(AppDomain.CurrentDomain.GetData("DataPath").ToString(), "Projects", request.Project); + + if (String.IsNullOrEmpty(request.Model)) + { + request.Model = Directory.GetDirectories(projectPath).Where(x => x.Contains("model_")).Last().Split(Path.DirectorySeparatorChar).Last(); + } + + var modelPath = Path.Combine(projectPath, request.Model); + + var agent = _platform.LoadAgentFromFile(modelPath); + + var aIResponse = _platform.TextRequest(new AIRequest + { + AgentDir = projectPath, + Model = request.Model, + Query = new String[] { request.Text } + }); + + var rasaResponse = new RasaResponse + { + Intent = new RasaResponseIntent + { + Name = aIResponse.Result.Metadata.IntentName, + Confidence = aIResponse.Result.Score + }, + Entities = aIResponse.Result.Entities.Select(x => new RasaResponseEntity + { + Extractor = x.Extrator, + Start = x.Start, + Entity = x.Entity, + Value = x.Value + }).ToList(), + Text = request.Text, + Model = request.Model, + Project = agent.Name, + IntentRanking = new List + { + new RasaResponseIntent + { + Name = aIResponse.Result.Metadata.IntentName, + Confidence = aIResponse.Result.Score + } + }, + Fullfillment = aIResponse.Result.Fulfillment + }; + + return rasaResponse;*/ + + return null; + } + } +} diff --git a/BotSharp.Platform.Rasa/Controllers/StatusController.cs b/BotSharp.Platform.Rasa/Controllers/StatusController.cs new file mode 100644 index 00000000..134b4c20 --- /dev/null +++ b/BotSharp.Platform.Rasa/Controllers/StatusController.cs @@ -0,0 +1,65 @@ +using BotSharp.Core.Engines; +using BotSharp.Platform.Rasa.Models; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace BotSharp.Platform.Rasa.Controllers +{ + /// + /// This returns all the currently available projects. + /// + [Route("[controller]")] + public class StatusController : ControllerBase + { + /// + /// Returns a list of available projects the server can use to fulfill /parse requests. + /// + /// + [HttpGet] + public ActionResult Get() + { + var status = new RasaStatusModel(); + status.AvailableProjects = JObject.FromObject(new { }); + status.MaxTrainingProcesses = 1; + + // scan dir, get all models + var projectPath = Path.Combine(AppDomain.CurrentDomain.GetData("DataPath").ToString(), "Projects"); + + if (!Directory.Exists(projectPath)) + { + Directory.CreateDirectory(projectPath); + } + + var projectDirs = Directory.GetDirectories(projectPath); + for(int idx = 0; idx < projectDirs.Length; idx++) + { + string project = projectDirs[idx].Split('\\').Last(); + var modelDirs = Directory.GetDirectories(projectDirs[idx]); + + List availableModels = new List(); + + for (int mIdx = 0; mIdx < modelDirs.Length; mIdx++) + { + string model = modelDirs[mIdx].Split('\\').Last(); + if (model.StartsWith(project + "_")) + { + availableModels.Add(model); + } + } + + status.AvailableProjects.Add(project, JObject.FromObject(new RasaProjectModel + { + Status = "ready", + AvailableModels = availableModels + })); + } + + return Ok(status); + } + } +} diff --git a/BotSharp.Platform.Rasa/Controllers/TrainController.cs b/BotSharp.Platform.Rasa/Controllers/TrainController.cs new file mode 100644 index 00000000..7e49b8d4 --- /dev/null +++ b/BotSharp.Platform.Rasa/Controllers/TrainController.cs @@ -0,0 +1,184 @@ +using BotSharp.Core.Engines; +using BotSharp.Platform.Abstraction; +using BotSharp.Platform.Models; +using BotSharp.Platform.Models.MachineLearning; +using BotSharp.Platform.Rasa.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace BotSharp.Platform.Rasa.Controllers +{ + /// + /// You can post your training data to this endpoint to train a new model for a project. + /// This request will wait for the server answer: either the model was trained successfully or the training exited with an error. + /// + [Route("[controller]")] + public class TrainController : ControllerBase + { + private RasaAi builder; + private readonly IPlatformSettings settings; + + public TrainController(RasaAi configuration, IPlatformSettings settings) + { + builder = configuration; + this.settings = settings; + } + + /// + /// Using the HTTP server, you must specify the project you want to train a new model for to be able to use it during parse requests later on : /train?project=my_project. + /// + /// Model name + /// Agent name or agent id + /// + [HttpPost] + public async Task> Train([FromQuery] string project, [FromQuery] string model) + { + string agentDir = Path.Combine(AppDomain.CurrentDomain.GetData("DataPath").ToString(), "Projects", project); + if (!Directory.Exists(agentDir)) + { + Directory.CreateDirectory(agentDir); + } + + string body = ""; + using (var reader = new StreamReader(Request.Body)) + { + body = reader.ReadToEnd(); + } + + var agent = await ImportAgent(project, body); + + var corpus = await builder.ExtractorCorpus(agent); + + var meta = await builder.Train(agent, corpus, new BotTrainOptions { Model = model }); + + return meta; + + } + + private async Task ImportAgent(string project, string body) + { + Console.WriteLine($"Update agent from http post, data length: {body.Length}"); + + // save to file + // save corpus to agent dir + var projectPath = Path.Combine(AppDomain.CurrentDomain.GetData("DataPath").ToString(), "Projects", project); + var rawPath = Path.Combine(projectPath, "tmp"); + + // clear tmp dir + if (Directory.Exists(rawPath)) + { + Directory.Delete(rawPath, true); + } + + Directory.CreateDirectory(rawPath); + + // Save raw data to file, then parse it to Agent instance. + var metaFileName = Path.Combine(rawPath, "meta.json"); + System.IO.File.WriteAllText(metaFileName, JsonConvert.SerializeObject(new AgentImportHeader + { + Name = project, + Platform = PlatformType.Rasa, + Id = Guid.NewGuid().ToString() + }, new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new CamelCasePropertyNamesContractResolver() + })); + + // in order to unify the process. + var fileName = Path.Combine(rawPath, "corpus.json"); + + System.IO.File.WriteAllText(fileName, body); + + /*string lang = Regex.Match(body, @"language:.+")?.Value; + if (!String.IsNullOrEmpty(lang)) + { + lang = lang.Substring(11, 2); + } + string data = Regex.Match(body, @"data:([\s\S]*)")?.Value; + if (String.IsNullOrEmpty(data)) + { + data = body; + } + else + { + data = data.Substring(6); + }*/ + + /*var agent = builder.GetAgentById(project); + + if (agent == null) + { + agent = builder.GetAgentByName(project); + } + + var corpus = builder.ExtractorCorpus(agent); + + var meta = await builder.Train(agent, corpus);*/ + + // var rasa_nlu_data = JsonConvert.DeserializeObject(data); + //rasa_nlu_data.Model = model; + //rasa_nlu_data.Project = project; + + var agent = await builder.LoadAgentFromFile>(rawPath); + await builder.SaveAgent(agent); + + return agent; + } + + private async Task> Train([FromBody] RasaTrainRequestViewModel request, [FromQuery] string project) + { + var trainer = new BotTrainer(settings); + if (String.IsNullOrEmpty(request.Project)) + { + request.Project = project; + } + + // save corpus to agent dir + var projectPath = Path.Combine(AppDomain.CurrentDomain.GetData("DataPath").ToString(), "Projects", project); + var modelPath = Path.Combine(projectPath, request.Model); + + if (!Directory.Exists(modelPath)) + { + Directory.CreateDirectory(modelPath); + } + + // Save raw data to file, then parse it to Agent instance. + var metaFileName = Path.Combine(modelPath, "meta.json"); + System.IO.File.WriteAllText(metaFileName, JsonConvert.SerializeObject(new AgentImportHeader + { + Name = project + })); + // in order to unify the process. + var fileName = Path.Combine(modelPath, "corpus.json"); + + System.IO.File.WriteAllText(fileName, JsonConvert.SerializeObject(request, new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new CamelCasePropertyNamesContractResolver() + })); + + var agent = await builder.GetAgentByName(project); + + var info = await trainer.Train(agent, new BotTrainOptions + { + AgentDir = projectPath, + Model = request.Model + }); + + return Ok(new { info = info.Model }); + } + } +} diff --git a/BotSharp.Platform.Rasa/Controllers/VersionController.cs b/BotSharp.Platform.Rasa/Controllers/VersionController.cs new file mode 100644 index 00000000..fc356271 --- /dev/null +++ b/BotSharp.Platform.Rasa/Controllers/VersionController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Controllers +{ +#if RASA + [Route("[controller]")] + public class VersionController : ControllerBase + { + [HttpGet] + public ActionResult Get() + { + return Ok(new RasaVersionModel + { + Version = "0.13.0", + MinimumCompatibleVersion = "0.13.0" + }); + } + } +#endif +} diff --git a/BotSharp.Platform.Rasa/Models/AgentModel.cs b/BotSharp.Platform.Rasa/Models/AgentModel.cs new file mode 100644 index 00000000..9f7652e7 --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/AgentModel.cs @@ -0,0 +1,20 @@ +using BotSharp.Platform.Models; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class AgentModel : AgentBase + { + [JsonProperty("common_examples")] + public List Intents { get; set; } + + [JsonProperty("entity_synonyms")] + public List Entities { get; set; } + + [JsonProperty("regex_features")] + public List Regex { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaAgentImportModel.cs b/BotSharp.Platform.Rasa/Models/RasaAgentImportModel.cs new file mode 100644 index 00000000..1cc5f345 --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaAgentImportModel.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaAgentImportModel + { + [JsonProperty("rasa_nlu_data")] + public AgentModel Data { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaConfigModel.cs b/BotSharp.Platform.Rasa/Models/RasaConfigModel.cs new file mode 100644 index 00000000..d40f3e34 --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaConfigModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaConfigModel + { + public string Config { get; set; } + + public string Data { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaIntentExpression.cs b/BotSharp.Platform.Rasa/Models/RasaIntentExpression.cs new file mode 100644 index 00000000..63b9a06f --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaIntentExpression.cs @@ -0,0 +1,13 @@ +using BotSharp.Platform.Models; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaIntentExpression : TrainingIntentExpression + { + + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaIntentExpressionPart.cs b/BotSharp.Platform.Rasa/Models/RasaIntentExpressionPart.cs new file mode 100644 index 00000000..6adcd39c --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaIntentExpressionPart.cs @@ -0,0 +1,12 @@ +using BotSharp.Core.Engines; +using BotSharp.Platform.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaIntentExpressionPart : TrainingIntentExpressionPart + { + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaOptions.cs b/BotSharp.Platform.Rasa/Models/RasaOptions.cs new file mode 100644 index 00000000..9e12933e --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaOptions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaOptions + { + public string HostUrl { get; set; } + public String[] Assembles { get; set; } + public string ContentRootPath { get; set; } + public String DbName { get; set; } + public String DbConnectionString { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaRequestExtension.cs b/BotSharp.Platform.Rasa/Models/RasaRequestExtension.cs new file mode 100644 index 00000000..9499a3e0 --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaRequestExtension.cs @@ -0,0 +1,272 @@ +using BotSharp.Core.Conversations; +using DotNetToolkit; +using EntityFrameworkCore.BootKit; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using BotSharp.Platform.Models.AiRequest; +using BotSharp.Platform.Models.Intents; +using BotSharp.Platform.Models.AiResponse; +using BotSharp.Platform.Models; + +namespace BotSharp.Platform.Rasa.Models +{ + public static class RasaRequestExtension + { + public static IntentResponse HandleIntentPerContextIn(AgentModel agent, AiRequest request, RasaResponse response, Database dc) + { + // Merge input contexts + /*var contexts = dc.Table() + .Where(x => x.ConversationId == request.SessionId && x.Lifespan > 0) + .ToList() + .Select(x => new AIContext { Name = x.Context.ToLower(), Lifespan = x.Lifespan }) + .ToList(); + + contexts.AddRange(request.Contexts.Select(x => new AIContext { Name = x.Name.ToLower(), Lifespan = x.Lifespan })); + contexts = contexts.OrderBy(x => x.Name).ToList();*/ + + // search all potential intents which input context included in contexts + /*var intents = agent.Intents.Where(it => + { + if (contexts.Count == 0) + { + return it.Contexts.Count() == 0; + } + else + { + return it.Contexts.Count() == 0 || + it.Contexts.Count(x => contexts.Select(ctx => ctx.Name).Contains(x.Name.ToLower())) == it.Contexts.Count; + } + }).OrderByDescending(x => x.Contexts.Count).ToList();*/ + + /*if (response.IntentRanking == null) + { + response.IntentRanking = new List + { + response.Intent + }; + } + + response.IntentRanking = response.IntentRanking.Where(x => x.Confidence > agent.MlConfig.MinConfidence).ToList(); + response.IntentRanking = response.IntentRanking.Where(x => intents.Select(i => i.Name).Contains(x.Name)).ToList();*/ + + // add Default Fallback Intent + /*if (response.IntentRanking.Count == 0) + { + var defaultFallbackIntent = agent.Intents.FirstOrDefault(x => x.Name == "Default Fallback Intent"); + response.IntentRanking.Add(new RasaResponseIntent + { + Name = defaultFallbackIntent.Name, + Confidence = decimal.Parse("0.8") + }); + }*/ + + response.Intent = response.IntentRanking.First(); + + var intent = (dc.Table().Where(x => x.AgentId == agent.Id && x.Name == response.Intent.Name) + .Include(x => x.Responses).ThenInclude(x => x.Contexts) + .Include(x => x.Responses).ThenInclude(x => x.Parameters).ThenInclude(x => x.Prompts) + .Include(x => x.Responses).ThenInclude(x => x.Messages)).First(); + + var intentResponse = ArrayHelper.GetRandom(intent.Responses); + intentResponse.IntentName = intent.Name; + + return intentResponse; + + } + + /// + /// + /// + /// + /// + /// + /// + /// Required field is missed + public static void HandleParameter(AgentModel agent, IntentResponse intentResponse, RasaResponse response, AiRequest aiRequest) + { + if (intentResponse == null) return; + + intentResponse.Parameters.ForEach(p => { + string query = aiRequest.Text; + var entity = response.Entities.FirstOrDefault(x => x.Entity == p.Name || x.Entity.Split(':').Contains(p.Name)); + if (entity != null) + { + p.Value = query.Substring(entity.Start, entity.End - entity.Start); + } + + // convert to Standard entity value + /*if (!String.IsNullOrEmpty(p.Value) && !p.DataType.StartsWith("sys.")) + { + p.Value = agent.Entities + .FirstOrDefault(x => x.Entity == p.DataType) + .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 + /*if (aiRequest.Entities != null) + { + var fixedEntity = request.Entities.FirstOrDefault(x => x.Name == p.Name); + if (fixedEntity != null) + { + if (query.ToLower().Contains(fixedEntity.Entries.First().Value.ToLower())) + { + p.Value = fixedEntity.Entries.First().Value; + } + } + }*/ + }); + } + + public static void HandleMessage(IntentResponse intentResponse) + { + if (intentResponse == null) return; + + var missingRequiredParameter = intentResponse.Parameters.FirstOrDefault(x => x.Required && String.IsNullOrEmpty(x.Value)); + if (missingRequiredParameter != null) + { + intentResponse.Messages = new List { + new IntentResponseMessage { + Type = AIResponseMessageType.Text, + Speech = ArrayHelper.GetRandom(missingRequiredParameter.Prompts).Prompt, + IntentResponseId = intentResponse.Id, + UpdatedTime = DateTime.UtcNow + } + }; + } + else + { + intentResponse.Messages = intentResponse.Messages.OrderBy(x => x.UpdatedTime).ToList(); + } + + intentResponse.Messages.ToList() + .ForEach(msg => + { + if (msg.Type == AIResponseMessageType.Custom) + { + + } + else + { + if (msg.Speech != "[]") + { + msg.Speech = msg.Speech.StartsWith("[") ? + ArrayHelper.GetRandom(msg.Speech.Substring(2, msg.Speech.Length - 4).Split(new string[] { "\",\"" }, StringSplitOptions.None).ToList()) : + msg.Speech; + + msg.Speech = ReplaceParameters4Response(intentResponse.Parameters, msg.Speech); + } + } + }); + } + + private static string ReplaceParameters4Response(List parameters, string text) + { + var reg = new Regex(@"\$\w+"); + + reg.Matches(text).Cast().ToList().ForEach(token => { + var parameter = parameters.FirstOrDefault(x => x.Name == token.Value.Substring(1)); + if(parameter != null) + { + text = text.Replace(token.Value, parameter?.Value?.ToString()); + } + }); + + return text; + } + + public static void HandleContext(Database dc, AiRequest aiRequest, IntentResponse intentResponse, AiResponse aiResponse) + { + if (intentResponse == null) return; + + // Merge context lifespan + // override if exists, otherwise add, delete if lifespan is zero + dc.DbTran(() => + { + var sessionContexts = dc.Table().Where(x => x.ConversationId == aiRequest.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().Remove(session1); + } + else + { + session1.Lifespan = ctx.Lifespan; + } + } + else + { + dc.Table().Add(new ConversationContext + { + ConversationId = aiRequest.SessionId, + Context = ctx.Name, + Lifespan = ctx.Lifespan + }); + } + }); + }); + + /*aiResponse.Result.Contexts = dc.Table() + .Where(x => x.Lifespan > 0 && x.ConversationId == AiConfig.SessionId) + .Select(x => new AIContext { Name = x.Context.ToLower(), Lifespan = x.Lifespan }) + .ToArray();*/ + } + + public static string GetModelPerContexts(AgentModel agent, AiRequest aiRequest, AiRequest request, Database dc) + { + // Merge input contexts + /*var contexts = dc.Table() + .Where(x => x.ConversationId == aiConfig.SessionId && x.Lifespan > 0) + .ToList() + .Select(x => new AIContext { Name = x.Context.ToLower(), Lifespan = x.Lifespan }) + .ToList(); + + contexts.AddRange(request.Contexts.Select(x => new AIContext { Name = x.Name.ToLower(), Lifespan = x.Lifespan })); + contexts = contexts.OrderBy(x => x.Name).ToList(); + + // search all potential intents which input context included in contexts + var intents = agent.Intents.Where(it => + { + if (contexts.Count == 0) + { + return it.Contexts.Count() == 0; + } + else + { + return it.Contexts.Count() > 0 && + it.Contexts.Count(x => contexts.Select(ctx => ctx.Name).Contains(x.Name.ToLower())) == it.Contexts.Count; + } + }).OrderByDescending(x => x.Contexts.Count).ToList(); + + // query per request contexts + var contextHashs = intents.Select(x => x.ContextHash).Distinct().ToList(); + + return contextHashs.FirstOrDefault();*/ + + return string.Empty; + } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaRequestModel.cs b/BotSharp.Platform.Rasa/Models/RasaRequestModel.cs new file mode 100644 index 00000000..4f7b42ad --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaRequestModel.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaRequestModel + { + [JsonProperty("q")] + public string Text { get; set; } + + public string Project { get; set; } + + public string Model { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaResponse.cs b/BotSharp.Platform.Rasa/Models/RasaResponse.cs new file mode 100644 index 00000000..8461ea7e --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaResponse.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaResponse + { + public RasaResponseIntent Intent { get; set; } + + //public AIResponseFulfillment Fullfillment { get; set; } + + [JsonProperty("intent_ranking")] + public List IntentRanking { get; set; } + + public List Entities { get; set; } + + public String Text { get; set; } + + public String Project { get; set; } + + public String Model { get; set; } + } + + public class RasaResponseIntent + { + public String Name { get; set; } + + public Decimal Confidence { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaResponseEntity.cs b/BotSharp.Platform.Rasa/Models/RasaResponseEntity.cs new file mode 100644 index 00000000..6a0ae753 --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaResponseEntity.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaResponseEntity : RasaIntentExpressionPart + { + public string Extractor { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaStatusModel.cs b/BotSharp.Platform.Rasa/Models/RasaStatusModel.cs new file mode 100644 index 00000000..112f1727 --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaStatusModel.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaStatusModel + { + [JsonProperty("available_projects")] + public JObject AvailableProjects { get; set; } + + [JsonProperty("current_training_processes")] + public int CurrentTrainingProcesses { get; set; } + + [JsonProperty("max_training_processes")] + public int MaxTrainingProcesses { get; set; } + } + + public class RasaProjectModel + { + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("current_training_processes")] + public int CurrentTrainingProcesses { get; set; } + + [JsonProperty("available_models")] + public List AvailableModels { get; set; } + + [JsonProperty("loaded_models")] + public List LoadedModels { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaTrainingData.cs b/BotSharp.Platform.Rasa/Models/RasaTrainingData.cs new file mode 100644 index 00000000..f44b61aa --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaTrainingData.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaTrainingData + { + [JsonProperty("common_examples")] + public List UserSays { get; set; } + + [JsonProperty("entity_synonyms")] + public List Entities { get; set; } + + [JsonProperty("regex_features")] + public List Regex { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaTrainingEntity.cs b/BotSharp.Platform.Rasa/Models/RasaTrainingEntity.cs new file mode 100644 index 00000000..7df3081c --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaTrainingEntity.cs @@ -0,0 +1,17 @@ +using BotSharp.Core.Engines; +using BotSharp.Platform.Models; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public sealed class RasaTrainingEntity + { + [JsonProperty("value")] + public String Entity { get; set; } + + public List Synonyms { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaTrainingRegex.cs b/BotSharp.Platform.Rasa/Models/RasaTrainingRegex.cs new file mode 100644 index 00000000..3c173afd --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaTrainingRegex.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaTrainingRegex + { + public String Name { get; set; } + + public String Pattern { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/Models/RasaVersionModel.cs b/BotSharp.Platform.Rasa/Models/RasaVersionModel.cs new file mode 100644 index 00000000..b93b1283 --- /dev/null +++ b/BotSharp.Platform.Rasa/Models/RasaVersionModel.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaVersionModel + { + public string Version { get; set; } + + [JsonProperty("minimum_compatible_version")] + public string MinimumCompatibleVersion { get; set; } + } +} diff --git a/BotSharp.Platform.Rasa/ModuleInjector.cs b/BotSharp.Platform.Rasa/ModuleInjector.cs new file mode 100644 index 00000000..c430792c --- /dev/null +++ b/BotSharp.Platform.Rasa/ModuleInjector.cs @@ -0,0 +1,29 @@ +using BotSharp.Core; +using BotSharp.Core.AgentStorage; +using BotSharp.Core.Modules; +using BotSharp.Platform.Rasa.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa +{ + public class ModuleInjector : IModule + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddSingleton>(); + AgentStorageServiceRegister.Register(services); + PlatformConfigServiceRegister.Register("rasaAi", services, config); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + + } + } +} diff --git a/BotSharp.Platform.Rasa/PlatformSettings.cs b/BotSharp.Platform.Rasa/PlatformSettings.cs new file mode 100644 index 00000000..9bec1404 --- /dev/null +++ b/BotSharp.Platform.Rasa/PlatformSettings.cs @@ -0,0 +1,11 @@ +using BotSharp.Core; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa +{ + public class PlatformSettings : PlatformSettingsBase + { + } +} diff --git a/BotSharp.Platform.Rasa/README.md b/BotSharp.Platform.Rasa/README.md new file mode 100644 index 00000000..3c1941f0 --- /dev/null +++ b/BotSharp.Platform.Rasa/README.md @@ -0,0 +1,2 @@ +# botsharp-rasa +BotSharp platform emulator extension which is compatible with RASA NLU. diff --git a/BotSharp.Platform.Rasa/RasaAi.cs b/BotSharp.Platform.Rasa/RasaAi.cs new file mode 100644 index 00000000..f1080949 --- /dev/null +++ b/BotSharp.Platform.Rasa/RasaAi.cs @@ -0,0 +1,138 @@ +using BotSharp.Core; +using BotSharp.Core.Engines; +using BotSharp.Platform.Abstraction; +using BotSharp.Platform.Models; +using BotSharp.Platform.Models.AiRequest; +using BotSharp.Platform.Models.AiResponse; +using BotSharp.Platform.Rasa.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace BotSharp.Platform.Rasa +{ + /// + /// Rasa nlu >= 0.12 + /// + public class RasaAi : + PlatformBuilderBase, + IPlatformBuilder + where TAgent : AgentModel + { + + public RasaAi(IAgentStorageFactory agentStorageFactory, IPlatformSettings settings) + : base(agentStorageFactory, settings) + { + + } + + + public async Task TextRequest(AiRequest request) + { + AiResponse aiResponse = new AiResponse(); + + /*string model = RasaRequestExtension.GetModelPerContexts(agent, AiConfig, request, dc); + var result = CallRasa(agent.Id, request.Query.First(), model); + + result.Content.Log(); + + RasaResponse response = result.Data; + aiResponse.Id = Guid.NewGuid().ToString(); + aiResponse.Lang = agent.Language; + aiResponse.Status = new AIResponseStatus { }; + aiResponse.SessionId = AiConfig.SessionId; + aiResponse.Timestamp = DateTime.UtcNow; + + var intentResponse = RasaRequestExtension.HandleIntentPerContextIn(agent, AiConfig, request, result.Data, dc); + + RasaRequestExtension.HandleParameter(agent, intentResponse, response, request); + + RasaRequestExtension.HandleMessage(intentResponse); + + aiResponse.Result = new AIResponseResult + { + Source = "agent", + ResolvedQuery = request.Query.First(), + Action = intentResponse?.Action, + Parameters = intentResponse?.Parameters?.ToDictionary(x => x.Name, x => (object)x.Value), + Score = response.Intent.Confidence, + Metadata = new AIResponseMetadata { IntentId = intentResponse?.IntentId, IntentName = intentResponse?.IntentName }, + Fulfillment = new AIResponseFulfillment + { + Messages = intentResponse?.Messages?.Select(x => { + if (x.Type == AIResponseMessageType.Custom) + { + return (new + { + x.Type, + Payload = JsonConvert.DeserializeObject(x.PayloadJson) + }) as Object; + } + else + { + return (new { x.Type, x.Speech }) as Object; + } + + }).ToList() + } + }; + + RasaRequestExtension.HandleContext(dc, AiConfig, intentResponse, aiResponse); + + Console.WriteLine(JsonConvert.SerializeObject(aiResponse.Result));*/ + + return aiResponse; + } + + public async Task ExtractorCorpus(TAgent agent) + { + var corpus = new TrainingCorpus() + { + Entities = new List(), + UserSays = new List>() + }; + + List entities = new List(); + + // generate entity list + agent.Intents.ForEach(intent => + { + intent.Entities.ForEach(entity => + { + if (!entities.Contains(entity.Entity)) + { + corpus.Entities.Add(new TrainingEntity + { + Entity = entity.Entity, + Values = new List + { + new TrainingEntitySynonym + { + Value = entity.Value, + Synonyms = new List + { + entity.Value + } + } + } + }); + entities.Add(entity.Entity); + } + }); + }); + + agent.Intents.ForEach(intent => + { + var express = new TrainingIntentExpression() + { + Text = intent.Text, + Intent = intent.Intent, + Entities = intent.Entities + }; + + corpus.UserSays.Add(express); + }); + + return corpus; + } + } +} diff --git a/BotSharp.Platform.Rasa/ViewModels/RasaTrainRequestViewModel.cs b/BotSharp.Platform.Rasa/ViewModels/RasaTrainRequestViewModel.cs new file mode 100644 index 00000000..e6950a7f --- /dev/null +++ b/BotSharp.Platform.Rasa/ViewModels/RasaTrainRequestViewModel.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Platform.Rasa.Models +{ + public class RasaTrainRequestViewModel + { + public string Project { get; set; } + + public string Model { get; set; } + + [JsonProperty("rasa_nlu_data")] + public RasaTrainingData Corpus { get; set; } + } +} diff --git a/BotSharp.WebHost/Settings/app.json b/BotSharp.WebHost/Settings/app.json index 486cfadc..80e7e1a3 100644 --- a/BotSharp.WebHost/Settings/app.json +++ b/BotSharp.WebHost/Settings/app.json @@ -10,6 +10,10 @@ "Name": "DialogflowAi", "Type": "BotSharp.Platform.Dialogflow" }, + { + "Name": "RasaAi", + "Type": "BotSharp.Platform.Rasa" + }, { "Name": "WeixinChannel", "Type": "BotSharp.Channel.Weixin" diff --git a/BotSharp.sln b/BotSharp.sln index c803e238..a4aeaff0 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Channel.Weixin", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Channel.FacebookMessenger", "BotSharp.Channel.FacebookMessenger\BotSharp.Channel.FacebookMessenger.csproj", "{22C52A04-581B-4186-8C04-0CD359FA568A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Platform.Rasa", "BotSharp.Platform.Rasa\BotSharp.Platform.Rasa.csproj", "{2EDE5F82-9219-4827-B636-13717DCDDF01}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution ARTICULATE|Any CPU = ARTICULATE|Any CPU @@ -231,6 +233,30 @@ Global {22C52A04-581B-4186-8C04-0CD359FA568A}.Test|Any CPU.Build.0 = Debug|Any CPU {22C52A04-581B-4186-8C04-0CD359FA568A}.Test|x64.ActiveCfg = Debug|Any CPU {22C52A04-581B-4186-8C04-0CD359FA568A}.Test|x64.Build.0 = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.ARTICULATE|Any CPU.ActiveCfg = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.ARTICULATE|Any CPU.Build.0 = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.ARTICULATE|x64.ActiveCfg = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.ARTICULATE|x64.Build.0 = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Debug|x64.ActiveCfg = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Debug|x64.Build.0 = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.DIALOGFLOW|Any CPU.ActiveCfg = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.DIALOGFLOW|Any CPU.Build.0 = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.DIALOGFLOW|x64.ActiveCfg = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.DIALOGFLOW|x64.Build.0 = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.RASA|Any CPU.ActiveCfg = Release|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.RASA|Any CPU.Build.0 = Release|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.RASA|x64.ActiveCfg = Release|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.RASA|x64.Build.0 = Release|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Release|Any CPU.Build.0 = Release|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Release|x64.ActiveCfg = Release|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Release|x64.Build.0 = Release|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Test|Any CPU.ActiveCfg = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Test|Any CPU.Build.0 = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Test|x64.ActiveCfg = Debug|Any CPU + {2EDE5F82-9219-4827-B636-13717DCDDF01}.Test|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE