From 1c5b714603d20c5c08a5a782a27b953d02a168a1 Mon Sep 17 00:00:00 2001 From: Oceania2018 Date: Thu, 12 Jul 2018 10:07:05 -0500 Subject: [PATCH] refactor IBotPlatform structure --- BotSharp.Core/Abstractions/IBotPlatform.cs | 5 +- BotSharp.Core/Agents/Agent.cs | 5 + BotSharp.Core/Agents/AgentDriver.cs | 1 + .../{AgentTrainConfig.cs => AgentMlConfig.cs} | 14 +- BotSharp.Core/BotSharp.Core.csproj | 8 +- BotSharp.Core/Conversations/Conversation.cs | 1 - .../Engines/Dialogflow/AIConfiguration.cs | 6 +- .../Engines/Dialogflow/AIDataService.cs | 179 ++++++++++++++++++ .../Dialogflow/AIResponseMessageType.cs | 1 + .../Engines/Dialogflow/AIServiceException.cs | 49 +++++ .../Dialogflow/AgentImporterInDialogflow.cs | 15 +- BotSharp.Core/Engines/Dialogflow/ApiAi.cs | 61 ++++++ BotSharp.Core/Engines/Dialogflow/ApiAiBase.cs | 75 ++++++++ .../Engines/Dialogflow/DialogflowAgent.cs | 4 + .../Dialogflow/Http/MultipartHttpClient.cs | 95 ++++++++++ BotSharp.Core/Engines/Rasa/RasaAi.cs | 141 ++++++++++---- .../RasaRequestExtension.cs} | 111 ++--------- BotSharp.Core/Intents/Intent.cs | 3 + BotSharp.Core/Intents/IntentEvent.cs | 20 ++ .../Intents/IntentResponseMessage.cs | 7 + BotSharp.UnitTest/AgentTest.cs | 15 +- BotSharp.UnitTest/BotSharp.UnitTest.csproj | 4 +- BotSharp.UnitTest/Settings/settings.bot.json | 2 +- 23 files changed, 664 insertions(+), 158 deletions(-) rename BotSharp.Core/Agents/{AgentTrainConfig.cs => AgentMlConfig.cs} (57%) create mode 100644 BotSharp.Core/Engines/Dialogflow/AIDataService.cs create mode 100644 BotSharp.Core/Engines/Dialogflow/AIServiceException.cs create mode 100644 BotSharp.Core/Engines/Dialogflow/ApiAi.cs create mode 100644 BotSharp.Core/Engines/Dialogflow/ApiAiBase.cs create mode 100644 BotSharp.Core/Engines/Dialogflow/Http/MultipartHttpClient.cs rename BotSharp.Core/Engines/{RequestExtension.cs => Rasa/RasaRequestExtension.cs} (69%) create mode 100644 BotSharp.Core/Intents/IntentEvent.cs diff --git a/BotSharp.Core/Abstractions/IBotPlatform.cs b/BotSharp.Core/Abstractions/IBotPlatform.cs index bba58ca4..48275a7a 100644 --- a/BotSharp.Core/Abstractions/IBotPlatform.cs +++ b/BotSharp.Core/Abstractions/IBotPlatform.cs @@ -1,4 +1,5 @@ -using System; +using BotSharp.Core.Models; +using System; using System.Collections.Generic; using System.Text; @@ -6,5 +7,7 @@ namespace BotSharp.Core.Engines { public interface IBotPlatform { + AIResponse TextRequest(AIRequest request); + void Train(); } } diff --git a/BotSharp.Core/Agents/Agent.cs b/BotSharp.Core/Agents/Agent.cs index 3b98dec3..e24ed2fb 100644 --- a/BotSharp.Core/Agents/Agent.cs +++ b/BotSharp.Core/Agents/Agent.cs @@ -67,5 +67,10 @@ namespace BotSharp.Core.Agents [Required] public DateTime CreatedDate { get; set; } + + public Boolean IsSkillSet { get; set; } + + [ForeignKey("AgentId")] + public AgentMlConfig MlConfig { get; set; } } } diff --git a/BotSharp.Core/Agents/AgentDriver.cs b/BotSharp.Core/Agents/AgentDriver.cs index 7b4079bd..ba48b6a5 100644 --- a/BotSharp.Core/Agents/AgentDriver.cs +++ b/BotSharp.Core/Agents/AgentDriver.cs @@ -31,6 +31,7 @@ namespace BotSharp.Core.Agents return dc.Table() .Include(x => x.Intents).ThenInclude(x => x.Contexts) .Include(x => x.Entities).ThenInclude(x => x.Entries).ThenInclude(x => x.Synonyms) + .Include(x => x.MlConfig) .FirstOrDefault(x => x.ClientAccessToken == aiConfig.ClientAccessToken || x.DeveloperAccessToken == aiConfig.ClientAccessToken); } diff --git a/BotSharp.Core/Agents/AgentTrainConfig.cs b/BotSharp.Core/Agents/AgentMlConfig.cs similarity index 57% rename from BotSharp.Core/Agents/AgentTrainConfig.cs rename to BotSharp.Core/Agents/AgentMlConfig.cs index 4b9660e1..e2d75ed6 100644 --- a/BotSharp.Core/Agents/AgentTrainConfig.cs +++ b/BotSharp.Core/Agents/AgentMlConfig.cs @@ -7,15 +7,21 @@ using System.Text; namespace BotSharp.Core.Agents { - [Table("Bot_AgentTrainConfig")] - public class AgentTrainConfig : DbRecord, IDbRecord + [Table("Bot_AgentMlConfig")] + public class AgentMlConfig : DbRecord, IDbRecord { [Required] [StringLength(36)] public String AgentId { get; set; } - public decimal ClassificationThreshould { get; set; } - + [Required] + public decimal MinConfidence { get; set; } + + [Required] + [MaxLength(64)] + public string CustomClassifierMode { get; set; } + + [MaxLength(64)] public String Pipeline { get; set; } } } diff --git a/BotSharp.Core/BotSharp.Core.csproj b/BotSharp.Core/BotSharp.Core.csproj index b73445d2..766f1957 100644 --- a/BotSharp.Core/BotSharp.Core.csproj +++ b/BotSharp.Core/BotSharp.Core.csproj @@ -32,11 +32,15 @@ TRACE;MODEL_PER_CONTEXTS + + + + - + - + diff --git a/BotSharp.Core/Conversations/Conversation.cs b/BotSharp.Core/Conversations/Conversation.cs index 14b2fd7f..645487e6 100644 --- a/BotSharp.Core/Conversations/Conversation.cs +++ b/BotSharp.Core/Conversations/Conversation.cs @@ -14,7 +14,6 @@ namespace BotSharp.Core.Conversations [StringLength(36)] public String AgentId { get; set; } - [Required] [StringLength(36)] public String UserId { get; set; } diff --git a/BotSharp.Core/Engines/Dialogflow/AIConfiguration.cs b/BotSharp.Core/Engines/Dialogflow/AIConfiguration.cs index 80e48a80..1d4978b4 100644 --- a/BotSharp.Core/Engines/Dialogflow/AIConfiguration.cs +++ b/BotSharp.Core/Engines/Dialogflow/AIConfiguration.cs @@ -6,10 +6,10 @@ namespace BotSharp.Core.Models { public class AIConfiguration { - private const string SERVICE_PROD_URL = ""; - private const string SERVICE_DEV_URL = ""; + private const string SERVICE_PROD_URL = "https://api.api.ai/v1/"; + private const string SERVICE_DEV_URL = "https://dev.api.ai/api/"; - private const string CURRENT_PROTOCOL_VERSION = "20190322"; + private const string CURRENT_PROTOCOL_VERSION = "20150910"; public string ClientAccessToken { get; private set; } diff --git a/BotSharp.Core/Engines/Dialogflow/AIDataService.cs b/BotSharp.Core/Engines/Dialogflow/AIDataService.cs new file mode 100644 index 00000000..ec82d678 --- /dev/null +++ b/BotSharp.Core/Engines/Dialogflow/AIDataService.cs @@ -0,0 +1,179 @@ +using BotSharp.Core.Engines.Dialogflow.Http; +using BotSharp.Core.Models; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Text; + +namespace BotSharp.Core.Engines.Dialogflow +{ + public class AIDataService + { + private readonly AIConfiguration config; + + public string SessionId { get; } + + public AIDataService(AIConfiguration config) + { + this.config = config; + + if (string.IsNullOrEmpty(config.SessionId)) + { + SessionId = Guid.NewGuid().ToString(); + } + else + { + SessionId = config.SessionId; + } + } + + public AIResponse Request(AIRequest request) + { + request.Language = config.Language.code; + request.Timezone = TimeZone.CurrentTimeZone.StandardName; + request.SessionId = SessionId; + + try + { + var httpRequest = (HttpWebRequest)WebRequest.Create(config.RequestUrl); + httpRequest.Method = "POST"; + httpRequest.ContentType = "application/json; charset=utf-8"; + httpRequest.Accept = "application/json"; + + httpRequest.Headers.Add("Authorization", "Bearer " + config.ClientAccessToken); + + var jsonSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }; + + var jsonRequest = JsonConvert.SerializeObject(request, Formatting.None, jsonSettings); + + if (config.DebugLog) + { + Debug.WriteLine("Request: " + jsonRequest); + } + + using (var streamWriter = new StreamWriter(httpRequest.GetRequestStream())) + { + streamWriter.Write(jsonRequest); + streamWriter.Close(); + } + + var httpResponse = httpRequest.GetResponse() as HttpWebResponse; + using (var streamReader = new StreamReader(httpResponse.GetResponseStream())) + { + var result = streamReader.ReadToEnd(); + + if (config.DebugLog) + { + Debug.WriteLine("Response: " + result); + } + + var aiResponse = JsonConvert.DeserializeObject(result); + + CheckForErrors(aiResponse); + + return aiResponse; + } + + } + catch (Exception e) + { + throw new AIServiceException(e); + } + } + + public AIResponse VoiceRequest(Stream voiceStream, RequestExtras requestExtras = null) + { + var request = new AIRequest(); + request.Language = config.Language.code; + request.Timezone = TimeZone.CurrentTimeZone.StandardName; + request.SessionId = SessionId; + + if (requestExtras != null) + { + requestExtras.CopyTo(request); + } + + try + { + var httpRequest = (HttpWebRequest)WebRequest.Create(config.RequestUrl); + httpRequest.Method = "POST"; + httpRequest.Accept = "application/json"; + + httpRequest.Headers.Add("Authorization", "Bearer " + config.ClientAccessToken); + + var jsonSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }; + + var jsonRequest = JsonConvert.SerializeObject(request, Formatting.None, jsonSettings); + + if (config.DebugLog) + { + Debug.WriteLine("Request: " + jsonRequest); + } + + var multipartClient = new MultipartHttpClient(httpRequest); + multipartClient.connect(); + + multipartClient.addStringPart("request", jsonRequest); + multipartClient.addFilePart("voiceData", "voice.wav", voiceStream); + + multipartClient.finish(); + + var responseJsonString = multipartClient.getResponse(); + + if (config.DebugLog) + { + Debug.WriteLine("Response: " + responseJsonString); + } + + var aiResponse = JsonConvert.DeserializeObject(responseJsonString); + + CheckForErrors(aiResponse); + + return aiResponse; + + } + catch (Exception e) + { + throw new AIServiceException(e); + } + } + + public bool ResetContexts() + { + var cleanRequest = new AIRequest("empty_query_for_resetting_contexts"); + cleanRequest.ResetContexts = true; + try + { + var response = Request(cleanRequest); + return !response.IsError; + } + catch (AIServiceException e) + { + Debug.WriteLine("Exception while contexts clean." + e); + return false; + } + } + + static void CheckForErrors(AIResponse aiResponse) + { + if (aiResponse == null) + { + throw new AIServiceException("API.AI response parsed as null. Check debug log for details."); + } + + if (aiResponse.IsError) + { + throw new AIServiceException(aiResponse); + } + } + } +} diff --git a/BotSharp.Core/Engines/Dialogflow/AIResponseMessageType.cs b/BotSharp.Core/Engines/Dialogflow/AIResponseMessageType.cs index 8c643889..dacb550b 100644 --- a/BotSharp.Core/Engines/Dialogflow/AIResponseMessageType.cs +++ b/BotSharp.Core/Engines/Dialogflow/AIResponseMessageType.cs @@ -7,6 +7,7 @@ namespace BotSharp.Core.Models public enum AIResponseMessageType { Text = 0, + Card = 1, Custom = 4 } } diff --git a/BotSharp.Core/Engines/Dialogflow/AIServiceException.cs b/BotSharp.Core/Engines/Dialogflow/AIServiceException.cs new file mode 100644 index 00000000..a87f86ed --- /dev/null +++ b/BotSharp.Core/Engines/Dialogflow/AIServiceException.cs @@ -0,0 +1,49 @@ +using BotSharp.Core.Models; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Core.Engines.Dialogflow +{ + public class AIServiceException : Exception + { + public AIResponse Response { get; set; } + + public AIServiceException() + { + } + + public AIServiceException(string message) : base(message) + { + } + + public AIServiceException(string message, Exception innerException) : base(message, innerException) + { + } + + public AIServiceException(Exception e) : base(e.Message, e) + { + } + + public AIServiceException(AIResponse response) + { + Response = response; + } + + public override string Message + { + get + { + if (Response != null && Response.IsError) + { + if (!string.IsNullOrEmpty(Response.Status.ErrorDetails)) + { + return Response.Status.ErrorDetails; + } + } + + return base.Message; + } + } + } +} diff --git a/BotSharp.Core/Engines/Dialogflow/AgentImporterInDialogflow.cs b/BotSharp.Core/Engines/Dialogflow/AgentImporterInDialogflow.cs index f867fb66..a43cfc6a 100644 --- a/BotSharp.Core/Engines/Dialogflow/AgentImporterInDialogflow.cs +++ b/BotSharp.Core/Engines/Dialogflow/AgentImporterInDialogflow.cs @@ -33,7 +33,12 @@ namespace BotSharp.Core.Engines agent.Id = Guid.NewGuid().ToString(); agent.Name = agentId; - return agent.ToObject(); + var result = agent.ToObject(); + result.MlConfig = agent.ToObject(); + result.MlConfig.MinConfidence = agent.MlMinConfidence; + result.MlConfig.AgentId = agent.Id; + + return result; } public void LoadCustomEntities(Agent agent, string agentDir) @@ -161,6 +166,8 @@ namespace BotSharp.Core.Engines Lifespan = x.Lifespan }).ToList(); + int millSeconds = 0; + newResponse.Messages = res.MessageList.Where(x => x.Speech != null || x.Payload != null) .Select(x => { @@ -170,7 +177,8 @@ namespace BotSharp.Core.Engines { Payload = JObject.FromObject(x.Payload), PayloadJson = JsonConvert.SerializeObject(x.Payload), - Type = x.Type + Type = x.Type, + UpdatedTime = DateTime.UtcNow.AddMilliseconds(millSeconds++) }; } else @@ -182,7 +190,8 @@ namespace BotSharp.Core.Engines return new IntentResponseMessage { Speech = speech, - Type = x.Type + Type = x.Type, + UpdatedTime = DateTime.UtcNow.AddMilliseconds(millSeconds++) }; } diff --git a/BotSharp.Core/Engines/Dialogflow/ApiAi.cs b/BotSharp.Core/Engines/Dialogflow/ApiAi.cs new file mode 100644 index 00000000..6f3c14b0 --- /dev/null +++ b/BotSharp.Core/Engines/Dialogflow/ApiAi.cs @@ -0,0 +1,61 @@ +using BotSharp.Core.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace BotSharp.Core.Engines.Dialogflow +{ + public class ApiAi : ApiAiBase, IBotPlatform + { + private readonly AIConfiguration config; + private readonly AIDataService dataService; + + public ApiAi(AIConfiguration config) + { + this.config = config; + + dataService = new AIDataService(this.config); + } + + public AIResponse TextRequest(string text) + { + if (string.IsNullOrEmpty(text)) + { + throw new ArgumentNullException("text"); + } + + return TextRequest(new AIRequest(text)); + } + + public AIResponse TextRequest(AIRequest request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + return dataService.Request(request); + } + + public AIResponse TextRequest(string text, RequestExtras requestExtras) + { + if (string.IsNullOrEmpty(text)) + { + throw new ArgumentNullException("text"); + } + + return TextRequest(new AIRequest(text, requestExtras)); + } + + public AIResponse VoiceRequest(Stream voiceStream, RequestExtras requestExtras = null) + { + if (config.Language == SupportedLanguage.Italian) + { + throw new AIServiceException("Sorry, but Italian language now is not supported in Speaktoit recognition. Please use some another speech recognition engine."); + } + + return dataService.VoiceRequest(voiceStream, requestExtras); + } + } +} diff --git a/BotSharp.Core/Engines/Dialogflow/ApiAiBase.cs b/BotSharp.Core/Engines/Dialogflow/ApiAiBase.cs new file mode 100644 index 00000000..82d130a6 --- /dev/null +++ b/BotSharp.Core/Engines/Dialogflow/ApiAiBase.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.Core.Engines.Dialogflow +{ + public class ApiAiBase + { + protected float[] TrimSilence(float[] samples) + { + if (samples == null) + { + return null; + } + + const float min = 0.000001f; + + var startIndex = 0; + var endIndex = samples.Length; + + for (var i = 0; i < samples.Length; i++) + { + + if (Math.Abs(samples[i]) > min) + { + startIndex = i; + break; + } + } + + for (var i = samples.Length - 1; i > 0; i--) + { + if (Math.Abs(samples[i]) > min) + { + endIndex = i; + break; + } + } + + if (endIndex <= startIndex) + { + return null; + } + + var result = new float[endIndex - startIndex]; + Array.Copy(samples, startIndex, result, 0, endIndex - startIndex); + return result; + + } + + protected static byte[] ConvertArrayShortToBytes(short[] array) + { + var numArray = new byte[array.Length * 2]; + Buffer.BlockCopy(array, 0, numArray, 0, numArray.Length); + return numArray; + } + + protected static short[] ConvertIeeeToPcm16(float[] source) + { + var resultBuffer = new short[source.Length]; + for (var i = 0; i < source.Length; i++) + { + var f = source[i] * 32768f; + + if (f > (double)short.MaxValue) + f = short.MaxValue; + else if (f < (double)short.MinValue) + f = short.MinValue; + resultBuffer[i] = Convert.ToInt16(f); + } + + return resultBuffer; + } + } +} diff --git a/BotSharp.Core/Engines/Dialogflow/DialogflowAgent.cs b/BotSharp.Core/Engines/Dialogflow/DialogflowAgent.cs index 74c6596c..fbfd7259 100644 --- a/BotSharp.Core/Engines/Dialogflow/DialogflowAgent.cs +++ b/BotSharp.Core/Engines/Dialogflow/DialogflowAgent.cs @@ -11,6 +11,10 @@ namespace BotSharp.Core.Adapters.Dialogflow public String Description { get; set; } public Boolean Published { get; set; } + public String DefaultTimezone { get; set; } public String Language { get; set; } + + public decimal MlMinConfidence { get; set; } + public string CustomClassifierMode { get; set; } } } diff --git a/BotSharp.Core/Engines/Dialogflow/Http/MultipartHttpClient.cs b/BotSharp.Core/Engines/Dialogflow/Http/MultipartHttpClient.cs new file mode 100644 index 00000000..157027dc --- /dev/null +++ b/BotSharp.Core/Engines/Dialogflow/Http/MultipartHttpClient.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; + +namespace BotSharp.Core.Engines.Dialogflow.Http +{ + public class MultipartHttpClient + { + private const string delimiter = "--"; + private string boundary = "SwA" + DateTime.UtcNow.Ticks.ToString("x") + "SwA"; + private HttpWebRequest request; + private BinaryWriter os; + + public MultipartHttpClient(HttpWebRequest request) + { + this.request = request; + } + + public void connect() + { + request.ContentType = "multipart/form-data; boundary=" + boundary; + request.SendChunked = true; + request.KeepAlive = true; + + os = new BinaryWriter(request.GetRequestStream(), Encoding.UTF8); + } + + public void addStringPart(string paramName, string data) + { + WriteString(delimiter + boundary + "\r\n"); + WriteString("Content-Type: application/json\r\n"); + WriteString("Content-Disposition: form-data; name=\"" + paramName + "\"\r\n"); + WriteString("\r\n" + data + "\r\n"); + } + + public void addFilePart(string paramName, string fileName, Stream data) + { + WriteString(delimiter + boundary + "\r\n"); + WriteString("Content-Disposition: form-data; name=\"" + paramName + "\"; filename=\"" + fileName + "\"\r\n"); + WriteString("Content-Type: audio/wav\r\n"); + + WriteString("\r\n"); + + int bufferSize = 4096; + byte[] buffer = new byte[bufferSize]; + + int bytesActuallyRead; + + bytesActuallyRead = data.Read(buffer, 0, bufferSize); + while (bytesActuallyRead > 0) + { + os.Write(buffer, 0, bytesActuallyRead); + bytesActuallyRead = data.Read(buffer, 0, bufferSize); + } + + WriteString("\r\n"); + } + + public void finish() + { + WriteString(delimiter + boundary + delimiter + "\r\n"); + os.Close(); + } + + private void WriteString(string str) + { + os.Write(Encoding.UTF8.GetBytes(str)); + } + + public string getResponse() + { + try + { + var httpResponse = request.GetResponse() as HttpWebResponse; + using (var streamReader = new StreamReader(httpResponse.GetResponseStream())) + { + var result = streamReader.ReadToEnd(); + return result; + } + } + catch (WebException we) + { + using (var stream = we.Response.GetResponseStream()) + { + using (var reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } + } + } +} diff --git a/BotSharp.Core/Engines/Rasa/RasaAi.cs b/BotSharp.Core/Engines/Rasa/RasaAi.cs index 74114f42..71530e07 100644 --- a/BotSharp.Core/Engines/Rasa/RasaAi.cs +++ b/BotSharp.Core/Engines/Rasa/RasaAi.cs @@ -42,47 +42,82 @@ namespace BotSharp.Core.Engines aiConfig.DevMode = agent.DeveloperAccessToken == aiConfig.ClientAccessToken; } - public string Train() + public AIResponse TextRequest(AIRequest request) { - var client = new RestClient($"{Database.Configuration.GetSection("Rasa:Nlu").Value}"); - var rest = new RestRequest("train", Method.POST); - rest.AddQueryParameter("project", agent.Id); + AIResponse aiResponse = new AIResponse(); - var corpus = agent.GrabCorpus(dc); +#if MODEL_PER_CONTEXTS + string model = RasaRequestExtension.GetModelPerContexts(agent, AiConfig, request, dc); + var result = CallRasa(agent.Id, request.Query.First(), model); +#else + var result = CallRasa(rasa.agent.Id, request.Query.First(), rasa.agent.Id); +#endif + result.Content.Log(); - string json = JsonConvert.SerializeObject(new { rasa_nlu_data = corpus }, - new JsonSerializerSettings + 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 => x.Value), + Score = response.Intent.Confidence, + Metadata = new AIResponseMetadata { IntentId = intentResponse?.IntentId, IntentName = intentResponse?.IntentName }, + Fulfillment = new AIResponseFulfillment { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - NullValueHandling = NullValueHandling.Ignore - }); + 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; + } - 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); + }).ToList() + } + }; - var response = client.Execute(rest); + RasaRequestExtension.HandleContext(dc, AiConfig, intentResponse, aiResponse); - if (response.IsSuccessful) - { - var result = JObject.Parse(response.Content); + Console.WriteLine(JsonConvert.SerializeObject(aiResponse.Result)); - string modelName = result["info"].Value().Split(": ")[1]; - - return modelName; - } - else - { - var result = JObject.Parse(response.Content); - - Console.WriteLine(result["error"]); - - return String.Empty; - } + return aiResponse; } - public void TrainWithContexts() + private IRestResponse CallRasa(string projectId, string text, string model) + { + var client = new RestClient($"{Database.Configuration.GetSection("Rasa:Nlu").Value}"); + + var rest = new RestRequest("parse", Method.POST); + string json = JsonConvert.SerializeObject(new { Project = projectId, Q = text, Model = model }, + new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + rest.AddParameter("application/json", json, ParameterType.RequestBody); + + return client.Execute(rest); + } + + public void Train() { var corpus = agent.GrabCorpus(dc); var client = new RestClient($"{Database.Configuration.GetSection("Rasa:Nlu").Value}"); @@ -174,7 +209,7 @@ namespace BotSharp.Core.Engines 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 trainingConfig = agent.Language == "zh" ? "config_jieba_mitie_sklearn.yml" : "config_mitie_sklearn.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); @@ -196,5 +231,47 @@ namespace BotSharp.Core.Engines }); } + + [Obsolete] + public string TrainWithoutContext() + { + var client = new RestClient($"{Database.Configuration.GetSection("Rasa:Nlu").Value}"); + var rest = new RestRequest("train", Method.POST); + rest.AddQueryParameter("project", agent.Id); + + var corpus = agent.GrabCorpus(dc); + + string json = JsonConvert.SerializeObject(new { rasa_nlu_data = corpus }, + new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore + }); + + 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); + + var response = client.Execute(rest); + + if (response.IsSuccessful) + { + var result = JObject.Parse(response.Content); + + string modelName = result["info"].Value().Split(": ")[1]; + + return modelName; + } + else + { + var result = JObject.Parse(response.Content); + + Console.WriteLine(result["error"]); + + return String.Empty; + } + } + } } diff --git a/BotSharp.Core/Engines/RequestExtension.cs b/BotSharp.Core/Engines/Rasa/RasaRequestExtension.cs similarity index 69% rename from BotSharp.Core/Engines/RequestExtension.cs rename to BotSharp.Core/Engines/Rasa/RasaRequestExtension.cs index 10396bf8..0a1a063f 100644 --- a/BotSharp.Core/Engines/RequestExtension.cs +++ b/BotSharp.Core/Engines/Rasa/RasaRequestExtension.cs @@ -18,80 +18,18 @@ using System.Text.RegularExpressions; namespace BotSharp.Core.Engines { - public static class RequestExtension + public static class RasaRequestExtension { public static AIResponse TextRequest(this RasaAi rasa, string text, RequestExtras requestExtras) { return rasa.TextRequest(new AIRequest(text, requestExtras)); } - public static AIResponse TextRequest(this RasaAi rasa, AIRequest request) + public static IntentResponse HandleIntentPerContextIn(Agent agent, AIConfiguration aiConfig, AIRequest request, RasaResponse response, Database dc) { - 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 - result.Content.Log(); - - RasaResponse response = result.Data; - aiResponse.Id = Guid.NewGuid().ToString(); - aiResponse.Lang = rasa.agent.Language; - aiResponse.Status = new AIResponseStatus { }; - aiResponse.SessionId = rasa.AiConfig.SessionId; - aiResponse.Timestamp = DateTime.UtcNow; - - var intentResponse = HandleIntentPerContextIn(rasa, request, result.Data); - HandleParameter(rasa.agent, intentResponse, response, request); - - HandleMessage(intentResponse); - - aiResponse.Result = new AIResponseResult - { - Source = "agent", - ResolvedQuery = request.Query.First(), - Action = intentResponse?.Action, - Parameters = intentResponse?.Parameters?.ToDictionary(x => x.Name, x=> 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() - } - }; - - HandleContext(dc, rasa, intentResponse, aiResponse); - - Console.WriteLine(JsonConvert.SerializeObject(aiResponse.Result)); - - return aiResponse; - } - - private static IntentResponse HandleIntentPerContextIn(RasaAi rasa, AIRequest request, RasaResponse response) - { - Database dc = rasa.dc; - // Merge input contexts var contexts = dc.Table() - .Where(x => x.ConversationId == rasa.AiConfig.SessionId && x.Lifespan > 0) + .Where(x => x.ConversationId == aiConfig.SessionId && x.Lifespan > 0) .ToList() .Select(x => new AIContext { Name = x.Context.ToLower(), Lifespan = x.Lifespan }) .ToList(); @@ -100,7 +38,7 @@ namespace BotSharp.Core.Engines contexts = contexts.OrderBy(x => x.Name).ToList(); // search all potential intents which input context included in contexts - var intents = rasa.agent.Intents.Where(it => + var intents = agent.Intents.Where(it => { if (contexts.Count == 0) { @@ -121,13 +59,13 @@ namespace BotSharp.Core.Engines }; } - response.IntentRanking = response.IntentRanking.Where(x => x.Confidence > decimal.Parse("0.3")).ToList(); + 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 = rasa.agent.Intents.FirstOrDefault(x => x.Name == "Default Fallback Intent"); + var defaultFallbackIntent = agent.Intents.FirstOrDefault(x => x.Name == "Default Fallback Intent"); response.IntentRanking.Add(new RasaResponseIntent { Name = defaultFallbackIntent.Name, @@ -137,7 +75,7 @@ namespace BotSharp.Core.Engines response.Intent = response.IntentRanking.First(); - var intent = (dc.Table().Where(x => x.AgentId == rasa.agent.Id && x.Name == response.Intent.Name) + 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(); @@ -157,7 +95,7 @@ namespace BotSharp.Core.Engines /// /// /// Required field is missed - private static void HandleParameter(Agent agent, IntentResponse intentResponse, RasaResponse response, AIRequest request) + public static void HandleParameter(Agent agent, IntentResponse intentResponse, RasaResponse response, AIRequest request) { if (intentResponse == null) return; @@ -197,7 +135,7 @@ namespace BotSharp.Core.Engines }); } - private static void HandleMessage(IntentResponse intentResponse) + public static void HandleMessage(IntentResponse intentResponse) { if (intentResponse == null) return; @@ -254,7 +192,7 @@ namespace BotSharp.Core.Engines return text; } - private static void HandleContext(Database dc, RasaAi rasa, IntentResponse intentResponse, AIResponse aiResponse) + public static void HandleContext(Database dc, AIConfiguration AiConfig, IntentResponse intentResponse, AIResponse aiResponse) { if (intentResponse == null) return; @@ -262,7 +200,7 @@ namespace BotSharp.Core.Engines // override if exists, otherwise add, delete if lifespan is zero dc.DbTran(() => { - var sessionContexts = dc.Table().Where(x => x.ConversationId == rasa.AiConfig.SessionId).ToList(); + var sessionContexts = dc.Table().Where(x => x.ConversationId == AiConfig.SessionId).ToList(); // minus 1 round sessionContexts.Where(x => !intentResponse.Contexts.Select(ctx => ctx.Name).Contains(x.Context)) @@ -288,7 +226,7 @@ namespace BotSharp.Core.Engines { dc.Table().Add(new ConversationContext { - ConversationId = rasa.AiConfig.SessionId, + ConversationId = AiConfig.SessionId, Context = ctx.Name, Lifespan = ctx.Lifespan }); @@ -297,33 +235,16 @@ namespace BotSharp.Core.Engines }); aiResponse.Result.Contexts = dc.Table() - .Where(x => x.Lifespan > 0 && x.ConversationId == rasa.AiConfig.SessionId) + .Where(x => x.Lifespan > 0 && x.ConversationId == AiConfig.SessionId) .Select(x => new AIContext { Name = x.Context.ToLower(), Lifespan = x.Lifespan }) .ToArray(); } - private static IRestResponse CallRasa(string projectId, string text, string model) + public static string GetModelPerContexts(Agent agent, AIConfiguration aiConfig, AIRequest request, Database dc) { - var client = new RestClient($"{Database.Configuration.GetSection("Rasa:Nlu").Value}"); - - var rest = new RestRequest("parse", Method.POST); - string json = JsonConvert.SerializeObject(new { Project = projectId, Q = text, Model = model }, - new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); - rest.AddParameter("application/json", json, ParameterType.RequestBody); - - return client.Execute(rest); - } - - private static string GetModelPerContexts(RasaAi rasa, AIRequest request) - { - Database dc = rasa.dc; - // Merge input contexts var contexts = dc.Table() - .Where(x => x.ConversationId == rasa.AiConfig.SessionId && x.Lifespan > 0) + .Where(x => x.ConversationId == aiConfig.SessionId && x.Lifespan > 0) .ToList() .Select(x => new AIContext { Name = x.Context.ToLower(), Lifespan = x.Lifespan }) .ToList(); @@ -332,7 +253,7 @@ namespace BotSharp.Core.Engines contexts = contexts.OrderBy(x => x.Name).ToList(); // search all potential intents which input context included in contexts - var intents = rasa.agent.Intents.Where(it => + var intents = agent.Intents.Where(it => { if (contexts.Count == 0) { diff --git a/BotSharp.Core/Intents/Intent.cs b/BotSharp.Core/Intents/Intent.cs index 162caa5d..682b9b81 100644 --- a/BotSharp.Core/Intents/Intent.cs +++ b/BotSharp.Core/Intents/Intent.cs @@ -28,6 +28,9 @@ namespace BotSharp.Core.Intents [ForeignKey("IntentId")] public List Contexts { get; set; } + [ForeignKey("IntentId")] + public List Events { get; set; } + /// /// Get input contexts hash /// diff --git a/BotSharp.Core/Intents/IntentEvent.cs b/BotSharp.Core/Intents/IntentEvent.cs new file mode 100644 index 00000000..6098db43 --- /dev/null +++ b/BotSharp.Core/Intents/IntentEvent.cs @@ -0,0 +1,20 @@ +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_IntentEvent")] + public class IntentEvent : DbRecord, IDbRecord + { + [Required] + [StringLength(36)] + public String IntentId { get; set; } + + [MaxLength(64)] + public String Name { get; set; } + } +} diff --git a/BotSharp.Core/Intents/IntentResponseMessage.cs b/BotSharp.Core/Intents/IntentResponseMessage.cs index e0d3799d..7cac1463 100644 --- a/BotSharp.Core/Intents/IntentResponseMessage.cs +++ b/BotSharp.Core/Intents/IntentResponseMessage.cs @@ -19,6 +19,11 @@ namespace BotSharp.Core.Intents public AIResponseMessageType Type { get; set; } + /// + /// Platform like: facebook, slack + /// + public String Platform { get; set; } + /// /// json list data /// @@ -31,6 +36,8 @@ namespace BotSharp.Core.Intents [MaxLength(1024)] public String PayloadJson { get; set; } + public String CardJson { get; set; } + [NotMapped] public JObject Payload { get; set; } } diff --git a/BotSharp.UnitTest/AgentTest.cs b/BotSharp.UnitTest/AgentTest.cs index 7853cbab..1792906e 100644 --- a/BotSharp.UnitTest/AgentTest.cs +++ b/BotSharp.UnitTest/AgentTest.cs @@ -70,20 +70,7 @@ namespace BotSharp.UnitTest var rasa = new RasaAi(dc, config); - string msg = rasa.Train(); - - Assert.IsTrue(!String.IsNullOrEmpty(msg)); - } - - [TestMethod] - public void TrainAgentPerContextTest() - { - var config = new AIConfiguration(BOT_CLIENT_TOKEN, SupportedLanguage.English); - config.SessionId = Guid.NewGuid().ToString(); - - var rasa = new RasaAi(dc, config); - - rasa.TrainWithContexts(); + rasa.Train(); } } } diff --git a/BotSharp.UnitTest/BotSharp.UnitTest.csproj b/BotSharp.UnitTest/BotSharp.UnitTest.csproj index aaa14a74..539f1223 100644 --- a/BotSharp.UnitTest/BotSharp.UnitTest.csproj +++ b/BotSharp.UnitTest/BotSharp.UnitTest.csproj @@ -31,8 +31,8 @@ - - + + diff --git a/BotSharp.UnitTest/Settings/settings.bot.json b/BotSharp.UnitTest/Settings/settings.bot.json index f96713c9..7d2c3951 100644 --- a/BotSharp.UnitTest/Settings/settings.bot.json +++ b/BotSharp.UnitTest/Settings/settings.bot.json @@ -1,6 +1,6 @@ { "Rasa": { - "Nlu": "http://gtx.local:5000" + "Nlu": "http://localhost:5000" }, "BotSharpAi": {