Skip to content

Instantly share code, notes, and snippets.

@nmfisher
Created November 18, 2018 01:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nmfisher/8f8e3b1f57111c97541f74ed7ffe5621 to your computer and use it in GitHub Desktop.
Save nmfisher/8f8e3b1f57111c97541f74ed7ffe5621 to your computer and use it in GitHub Desktop.
using Lexico.DataModel;
using Lexico.DataModel.Questionnaire;
using Lexico.Search.NETStandard;
using Lexico.Web.Multitenancy.NET461;
using Lexico.Web.Multitenancy.NET461.User;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Lexico.Services.Questionnaire;
using Lexico.Web.Questionnaire;
namespace Lexico.Web.Dialog
{
public class DialogStateBuilder : IDialogStateBuilder
{
public enum Instruction { Affirmative, Negative, Cancel, Reset, None };
public static Regex HelloRequestedRegex = new Regex(@"^(hello|hi)");
public static Regex CreateRegex = new Regex(@"^(create|make|new|give|generate)");
public static Regex HelpRegex = new Regex(@"^(help)");
public static Regex AffirmativeRegex = new Regex(@"^(yes|y|ok|confirm)");
public static Regex NegativeRegex = new Regex(@"^(no|n)$");
public static Regex ListRequestedRegex = new Regex(@"^(show|list)");
public static Regex ArticleRegex = new Regex(@"( the | a | an | new)");
public static Regex NextRegex = new Regex("(')(next)");
public static Regex ResetRegex = new Regex("^(cancel|quit|reset)");
protected readonly IQuestionnaireService QuestionnaireService;
protected readonly IApplicationUserIdentity ApplicationUserIdentity;
protected readonly IFuzzySearchService FuzzySearchService;
private readonly IWebConfigurationOptions WebConfigurationOptions;
private readonly IWebQuestionnaireService WebQuestionnaireService;
public DialogStateBuilder(IQuestionnaireService questionnaireService,
IFuzzySearchService fuzzySearchService,
IApplicationUserIdentity applicationUserIdentity,
IWebQuestionnaireService webQuestionnaireService,
IWebConfigurationOptions webConfigurationOptions)
{
FuzzySearchService = fuzzySearchService;
QuestionnaireService = questionnaireService;
ApplicationUserIdentity = applicationUserIdentity;
WebConfigurationOptions = webConfigurationOptions;
WebQuestionnaireService = webQuestionnaireService;
}
public IDialogState Initiate()
{
return new Null(this);
}
public Instruction Parse(string text)
{
var lower = text.ToLower();
if (AffirmativeRegex.IsMatch(lower))
return Instruction.Affirmative;
else if (NegativeRegex.IsMatch(lower))
return Instruction.Negative;
else if (ResetRegex.IsMatch(lower))
return Instruction.Reset;
else
return Instruction.None;
}
protected string[] FormatResponse(IQuestionnaireItem questionnaireItem)
{
var questionText = questionnaireItem.QuestionText;
if (questionnaireItem.Type == QuestionnaireType.UserOption)
{
var conditions = questionnaireItem.ConditionalItems.ToList();
questionText += " [";
for (int i = 0; i < conditions.Count; i++)
{
questionText += conditions[i].Condition;
if (i < conditions.Count - 1)
questionText += " / ";
}
questionText += "]";
}
return new string[] {
questionText,
questionnaireItem.Required ? null : "This question isn't required - type 'next' to skip."
};
}
internal class Null : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public Null(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
dialog.States = new Stack<IDialogState>();
IDialogState nextState;
if (CreateRegex.IsMatch(interaction.ToLower()))
{
nextState = new CreateRequested(DialogStateBuilder);
} else if(ListRequestedRegex.IsMatch(interaction.ToLower()))
{
nextState = new ListRequested(DialogStateBuilder);
} else if(HelpRegex.IsMatch(interaction.ToLower()))
{
nextState = new HelpRequested(DialogStateBuilder);
} else if (HelloRequestedRegex.IsMatch(interaction.ToLower()))
{
nextState = new HelloRequested(DialogStateBuilder);
}
else
{
nextState = new Unknown(DialogStateBuilder);
}
dialog.IsProcessing = true;
dialog.States.Push(nextState);
}
}
internal class HelloRequested : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public HelloRequested(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
dialog.States = new Stack<IDialogState>();
dialog.States.Push(new Null(DialogStateBuilder));
dialog.Responses = new List<dynamic>()
{
"Hi!",
"Create a questionnaire by typing 'create a ' followed by the name of the questionnaire in your account",
"Type 'help' to get a link to our user guide.",
"Type 'list' to list all questionnaires in your account",
"You can also type 'help' to get a link to our user guide."
};
dialog.IsProcessing = false;
}
}
public class ResetRequested : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public ResetRequested(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
dialog.States.Pop();
dialog.States.Push(new ProcessingResetConfirmation(DialogStateBuilder));
dialog.Responses = new string[]
{
"Do you want to discard this conversation?"
};
dialog.IsProcessing = false;
}
}
public class ProcessingResetConfirmation : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public ProcessingResetConfirmation(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
var instruction = DialogStateBuilder.Parse(interaction);
dialog.IsProcessing = true;
if (instruction == Instruction.Affirmative)
{
dialog.States.Push(new ResetConfirmed(DialogStateBuilder));
} else
{
dialog.States.Pop();
}
}
}
public class ResetConfirmed : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public ResetConfirmed(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
dialog.States = new Stack<IDialogState>();
dialog.States.Push(new Null(DialogStateBuilder));
dialog.Outputs = null;
dialog.Inputs = null;
dialog.Responses = new string[]
{
"OK, I've reset everything."
};
dialog.IsProcessing = false;
}
}
public class ListRequested : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public ListRequested(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
var questionnaires = DialogStateBuilder.QuestionnaireService.List(DialogStateBuilder.ApplicationUserIdentity.User,
DialogStateBuilder.ApplicationUserIdentity.Organization)?.ToList();
var responses = new List<string>();
if (questionnaires == null || questionnaires.Count == 0)
responses.Add("Sorry, there don't seem to be any documents available.");
else {
responses.Add("The following questionnaires are available");
responses.AddRange(questionnaires.Select((x) => x.Name));
}
dialog.Responses = responses.Cast<dynamic>().ToList();
dialog.IsProcessing = false;
dialog.States = new Stack<IDialogState>();
dialog.States.Push(new Null(DialogStateBuilder));
}
}
public class HelpRequested : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public HelpRequested(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
dialog.States.Pop();
dialog.States.Push(new Null(DialogStateBuilder));
dialog.IsProcessing = false;
dialog.Responses = new List<dynamic>()
{
"Need some help? Please visit https://app.lexico.io/help for our user guide."
};
}
}
public class CreateRequested : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public CreateRequested(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
var target = CreateRegex.Replace(interaction, "");
target = ArticleRegex.Replace(target, "");
target = target.Trim();
var questionnaires = DialogStateBuilder.FuzzySearchService.Find(target,
DialogStateBuilder.ApplicationUserIdentity.User,
DialogStateBuilder.ApplicationUserIdentity.Organization);
dialog.States = new Stack<IDialogState>();
if (questionnaires == null || questionnaires.Count() == 0)
{
dialog.States.Push(new CreateNotFound(DialogStateBuilder));
}
else {
dialog.Inputs = new Stack<object>(questionnaires.Select((x) => x.Object).Reverse());
dialog.States.Push(new CreateConfirmationNeeded(DialogStateBuilder));
dialog.IsProcessing = true;
}
}
}
public class CreateConfirmationNeeded : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public CreateConfirmationNeeded(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
dialog.States.Pop();
dialog.Responses = new List<dynamic>()
{
"We found a " + (dialog.Inputs.Peek() as IQuestionnaire).Name + ", is this what you wanted?"
};
dialog.States.Push(new ProcessingCreateConfirmation(DialogStateBuilder));
dialog.IsProcessing = false;
}
}
public class ProcessingCreateConfirmation : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public ProcessingCreateConfirmation(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
var questionnaire = (IQuestionnaire)dialog.Inputs.Pop();
var instruction = DialogStateBuilder.Parse(interaction);
if (instruction == Instruction.Cancel || instruction == Instruction.Reset)
{
dialog.States.Push(new Null(DialogStateBuilder));
dialog.Responses = new string[] { "OK, cancelled." };
}
else if (instruction == Instruction.Affirmative)
{
dialog.Inputs.Push(new DialogQuestionnaireState(questionnaire));
dialog.Responses = new List<dynamic>()
{
"OK, we'll kick things off here - remember you can also create this @ " + DialogStateBuilder.WebConfigurationOptions.FormatCreateTemplateUrl(questionnaire.Id)
};
dialog.States.Push(new InputRequired(DialogStateBuilder));
dialog.IsProcessing = true;
}
else if (instruction == Instruction.Negative)
{
if (dialog.Inputs.Count == 0)
{
dialog.States.Push(new Null(DialogStateBuilder));
dialog.IsProcessing = false;
dialog.Responses = new string[] { "Sorry, we don't have any more documents." };
}
else
{
dialog.Responses = new string[] { ((IQuestionnaire)dialog.Inputs.Peek()).Name + "?" };
dialog.IsProcessing = false;
}
}
else
{
throw new NotSupportedException();
}
}
}
public class CreateNotFound : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public CreateNotFound(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
dialog.Responses = new string[] { "Sorry, we couldn't find a questionnaire under that name." };
dialog.States.Push(new Null(DialogStateBuilder));
dialog.IsProcessing = false;
}
}
public class ProcessingInput : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public ProcessingInput(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
var instruction = DialogStateBuilder.Parse(interaction);
if (instruction == Instruction.Reset) {
dialog.States.Push(new ResetRequested(DialogStateBuilder));
dialog.IsProcessing = true;
return;
};
var questionnaireState = (DialogQuestionnaireState)dialog.Inputs.Peek();
try
{
questionnaireState.SetResponse(interaction);
}
catch (InvalidOptionException e)
{
var responses = new List<dynamic> {
"Sorry, that wasn't one of the options. Please try again",
};
responses.AddRange(DialogStateBuilder.FormatResponse(questionnaireState.Current));
dialog.Responses = responses;
dialog.IsProcessing = false;
}
catch (InvalidResponseException e)
{
var responses = new List<dynamic> {
"That question is marked as required and needs a non-empty response.",
};
responses.AddRange(DialogStateBuilder.FormatResponse(questionnaireState.Current));
dialog.Responses = responses;
dialog.IsProcessing = false;
}
dialog.States.Push(new InputRequired(DialogStateBuilder));
dialog.IsProcessing = true;
}
}
public class InputRequired : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public InputRequired(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
var questionnaireState = (DialogQuestionnaireState)dialog.Inputs.Peek();
if (!questionnaireState.HasNext())
{
dialog.States.Push(new CreateComplete(DialogStateBuilder));
dialog.IsProcessing = true;
return;
}
foreach (var response in DialogStateBuilder.FormatResponse(questionnaireState.Current))
{
dialog.Responses.Add(response);
}
dialog.States.Pop();
dialog.States.Push(new ProcessingInput(DialogStateBuilder));
dialog.IsProcessing = false;
}
}
public class CreateComplete : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public CreateComplete(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
var dialogResponses = new List<dynamic>() { "We're done!" };
var questionnaireState = (DialogQuestionnaireState)dialog.Inputs.Pop();
var responses = DialogStateBuilder.WebQuestionnaireService.Apply(questionnaireState.Build(),
DialogStateBuilder.ApplicationUserIdentity);
foreach(var resp in responses)
{
if (resp is string)
dialogResponses.Add(resp);
else {
// todo - we should use strong types for these responses with an associated formatter
if (resp.recipients != null)
{
if (resp.recipients.email != null && (resp.recipients.email as IEnumerable<string>).Count() > 0)
{
dialogResponses.Add("Your document has been e-mailed to the following recipients:");
foreach(var address in resp.recipients.email)
dialogResponses.Add(address);
if (resp.recipients.slack != null && (resp.recipients.slack as IEnumerable<string>).Count() > 0)
{
dialogResponses.Add("...and posted to the following Slack channels:");
foreach (var address in resp.recipients.slack)
dialogResponses.Add(address);
}
} else
{
if (resp.recipients.slack != null && (resp.recipients.slack as IEnumerable<string>).Count() > 0)
{
dialogResponses.Add("Your document has been posted to the following Slack channels:");
foreach (var address in resp.recipients.slack)
dialogResponses.Add(address);
}
}
if(resp.downloadUrl != null)
{
dialogResponses.Add("You can download your document by clicking on the following url:");
dialogResponses.Add(resp.downloadUrl);
}
} else
{
throw new NotSupportedException();
}
}
}
dialog.Responses = dialogResponses;
dialog.States.Push(new Null(DialogStateBuilder));
dialog.IsProcessing = false;
}
}
public class Unknown : IDialogState
{
private readonly DialogStateBuilder DialogStateBuilder;
public Unknown(DialogStateBuilder dialogStateBuilder)
{
DialogStateBuilder = dialogStateBuilder;
}
public void Process(IDialog dialog, string interaction)
{
dialog.States = new Stack<IDialogState>();
dialog.States.Push(new Null(DialogStateBuilder));
dialog.Responses = new string[] { "Sorry, I didn't understand that. " };
dialog.IsProcessing = false;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment