Atura Form Engine

Sometimes a client will approach Atura with the need to create an AI assistant to help with a specific process. The Atura Form Engine is a set of reusable components that allows us to create assistants that can gather a large set of forms data from a user via chat. It presents the end user with a pleasant conversation-style format while still gathering the information needed to "complete a form" - a loan application or quote request for example.

Usually when designing an AI assistant, dialogs are used to create interactions with a user, while an NLP engine is used to interpret what the user says. However, when capturing large sets of structured data, such as the details for a motor vehicle quote, it may not be desirable to allow the user to enter free text at any point. Free text could mean "unnecessary" language processing and flow interpretation, which detracts from the core purpose of the AI assistant.

An intelligent form engine facilitates a great user experience, which means that users are more likely to complete what can be a tedious process. Logging in (through integrating with the client's systems) means that we can potentially retrieve information about the user and "prepopulate" many elements of a form. This means that a user doesn't need to re-enter information that is already on record, instead they are simply asked to confirm their details, which makes for a much better experience. Similarly, once a user has entered a specific detail, they are not asked to enter it again - even if they move to a different line of conversation. For example, as part of requesting an insurance quote once a user has been asked to enter their home address, they will not be asked again in that session. So they can request a quote for funeral insurance, and when they move on to requesting a vehicle insurance quote, the 'vehicle insurance quote flow' will not ask them for their address, saving time and frustration on the user's behalf.

Once the assistant is live, we can monitor where in the process users drop off, so we can tweak the dialogue or flow to make it more user friendly - improving the process and increasing the chance of completion.

Form Engine Components

The form engine was built leveraging the Microsoft Bot Builder Framework. It consists of multiple components:

  1. Fields - the form engine instantiates and renders these to the user.
  2. Validation - these are the rules used to determine whether a user has entered valid data in to a field, so that they may proceed to the next field in the section.
  3. Sections - container classes that hold multiple fields and their linked attributes. The form engine renders these in a specific order.
  4. Attributes - rules that determine when the form engine should render a field or section to the user.
  5. Functionality - occurs behind the scenes to facilitate the other components

The conversation is rendered in order of the fields you create and specify in sections, and will continue until all fields have been captured with predefined validated inputs.

Fields

Every custom field type and base field type inherit their properties from the base field Atura form Field.

Constructor and Prompt

Prompt is rendered based on constructor. If your field has multiple prompts before it captures an input, the form field will be initialized with an array of prompts. Here are the currently available constructors for the Atura Form Field:

protected AturaFormField(string[] defaultPrompts, string validationMessage)

protected AturaFormField(string defaultPrompt, string validationMessage)

protected AturaFormField(string defaultPrompt)

protected AturaFormField(string[] defaultPrompts)

Base Field Properties

The field properties most commonly used are listed below. You will override these when creating your assistant:

  • Label
  • Data type
  • Field type
  • Help message
  • Raw field value
  • Temp User Input to Validate
  • Field Is Being Replayed
  • Has Been Prepopulated
  • Validation Message
  1. Label - an easy to read field name.

    public override string Label { get; } = "Contact Number";

  2. Data Type - the base data type your field is collecting.

    public override Type DataType { get; } = typeof(string);

  3. Field Type - the way your message containing the field is rendered by the Form Engine (depends on its field type). You may create as many Custom Field types as needed.

    public override FieldType FieldType { get; } = FieldType.CurrencyInputField;

  4. Help Message - a helpful ‘hint text’ property on a field.

    public override string HelpMessage { get; set; } = "Your age can be calculated from your ID number";

  5. Raw Field Value - the default object which captures the users input in its original form for the field type.

    public virtual object RawFieldValue;

  6. Temp User Input to Validate - the base field stores the recently captured data from the user in this property until it passed as “valid” and confirmed, to be stored in the RawFieldValue.

    public string TempUserInputValidate { get; set; };

  7. Field Is Being Replayed - a Boolean that is set to true if the field has been previously rendered e.g. when then user types "back".

    public bool FieldIsSBeingReplayed { get; set; } = false;

  8. Has Been Prepopulated - a Boolean that is set to true if the field had its data prepopulated by another field's input earlier e.g. date of birth as derived from an ID number.

    public bool HasBeenPreposulated { get; set; } = false;

  9. Validation Message - a base-layer generic message such as "Not Valid" can be used. Further customising fields and the use of extensibility allows a custom field such as "CellphoneField" to use a more specific message - "Invalid number, please enter as follows +27 82 123 4455" for example.

Custom Field Types

Custom field types can be created from the base Atura form field type and reused over multiple fields depending on the flow and shared fields you wish to use and extend. Each additional field you add can have its own unique set of properties, which can then be extended as a base type.

IMAGE 1

Once the field layout has been decided, you can create it with its own overrides for base properties such as "Prompt" and "Validation Message". Below is an example of a custom field - Cell Numbers - type created to allow capture of a mobile phone number.

publicclassCellNumberField : AturaFormField
  {
    public CellNumberField(): base("May I pleae have your cellphone number?, Please try again")
  {
  }
    public override string Label { get; } = "Cell number";
    public override Type DataType { get; } = typeof(string);
}

Validation

Validation at a field level ensures that the information entered by a user is what is expected by the system. It ensures that the user-entered data is of the correct type and in the correct format as expected by the business. All validation is handled on a base layer, and then on a custom field layer if required.

public abstract Task\<FormValidationResult> ValidateInput(AturaDialogContext currentContext, object messageText, AturaFormField currentField, List\<IAturaFormElement> allFields);

The Form Validation result has three common properties which fields make use of when validating the captured user input. The ValidationMessage points to the property mentioned in the base Atura Form Field.

publicclassFormValidationResult
  {
    publicstring ValidationMessage { get; set; }
    publicobject NewValidRawValue { get; set; }
    publicbool IsValid { get; set; }
  }

The Atura Form Validation Result can be used to make further decisions in your flows, such as continuing to render the next field, or choosing to render the field and prompt again, asking the user to re-capture the supplied information.

In the following example, we will create a sample validator making use of the Fluent Validation framework to apply rules and test the validation result.

EXAMPLE: Simple Even Number Validator

To set up your base validator class, create a new class that inherits from AturaFormValidator and invoke the method Task\<FormValidationResult> ValidateInput

We will be using two validators in this example - ValidateInteger and ValidateNumberIsEven.

  • ValidateInteger - to verify 1) the captured user input is not empty and 2) there is no text other than numbers in the user's input and 3) the numbers are integer type, not doubles or floats.

  • ValidateNumberIsEven - to verify that 1) the result is not empty and 2) that the result is an even number

publicoverrideasync Task\<FormValidationResult> ValidateInput(AturaDialogContext context, object inputToValidate, AturaFormField currentField, List\<IAturaFormElement> allFields)
  {
    var fieldType = currentField.FieldType;
    var formValidationResult = new FormValidationResult
    {
      ValidationMessage = currentField.ValidationMessage
    };
    switch (fieldType)
    {
      case FieldType.IntegerInputField:
      var integerFormValidationResult = ValidateInteger((AturaIntegerFormField)currentField);
      if(integerFormValidationResult.IsValid)
      {
        return ValidateNumberIsEven((AturaMathField)currentField);
      }
      break;
    }
  }

Once you have your CustomFormValidator structure as described above, you can add the various field types your flows have been created with. A switch case for each base field type validator (ValidateInteger), and then custom field validator logic following that (ValidateNumberIsEven), can be extended and customized for all flows capturing user input.

To take a closer look at the rules and logic that are applied within the validators, we will look at the structure of the method that creates the ValidateInteger result.

privatestatic FormValidationResult ValidateInteger(AturaIntegerFormField fieldWithValue)
{
  var validator = new IntegerValidator();
  var integerResult = validator.Validate(fieldWithValue);
  var validationResult = new FormValidationResult
  {
    ValidationMessage = integerResult.Errors?.FirstOrDefault()?.ErrorMessage,
    NewValidRawValue = integerResult.IsValid ? validator.ValidRawValue : (object)null,
    IsValid = integerResult.IsValid
  };
  return validationResult;
}

Examining the logic that happens inside validator.Validate, we can see how the ValidateInteger class extends the AbstractValidator from the FluentValidation framework and runs rules accordingly for the class to return a result.

publicclassIntegerValidator : AbstractValidator\<AturaIntegerFormField>
  {
    public IntegerValidator()
    {
      RuleFor(field => field.TempUserInputToValidate).NotEmpty().WithMessage(field => field.ValidationMessage);
      RuleFor(field => field).Must(beValidInteger).WithMessage(field => field.ValidationMessage);
    }
    publicint ValidRawValue { get; privateset; }
    privatebool beValidInteger(AturaIntegerFormField field)
    {
      if (field.TempUserInputToValidate == null)
      {
        returnfalse;
      }
      var extractedNumbers = Regex.Match(field.TempUserInputToValidate, @"((\s|^)(\d+)(\s|$))")?.Value;
      if (int.TryParse(extractedNumbers, NumberStyles.Integer, CultureInfo.InvariantCulture, outint parsed))
      {
        if (parsed >= field.MinValue && parsed \<= field.MaxValue)
        {
          ValidRawValue = parsed;
          returntrue;
        }
      }
      returnfalse;
    }
  }

Now that CustomFormValidator has called the ValidateInteger method, we can use the result to check if we want to replay the field, show a validation message to the user or proceed to the next validation method, ValidateNumberIsEven.

publicclassValidateNumberIsEven : AbstractValidator\<AturaFormField>
  {
    public ValidateNumberIsEven()
    {
      RuleFor(field => field.TempUserInputToValidate).NotEmpty().WithMessage(field => field.ValidationMessage);
      RuleFor(field => field).Must(BeEvenNumber).WithMessage(field => field.ValidationMessage);
    }
    publicint ValidRawValue { get; privateset; }
    privatebool BeEvenNumber(AturaMathField field)
    {
      if (field.TempUserInputToValidate == null)
      {
        returnfalse;
      }
      if (field.TempUserInputToValidate % 2)
      {
        //field.TempUserInputToValidate is odd
        returnfalse;
      }
      returntrue;
    }
  }

Once all the validation results for the current field have passed in the CustomFormValidator, the form engine will continue to the next field in the flow.

Sections

Sections are the containers which hold the fields and associated rules to render them. The ordering of fields in your section will determine the order in which the form engine renders them in the flows.

There are two types of sections:

  • Atura Form Section: holds all fields required for a bot flow and attributes.
  • Atura Form Subsection: sits within a Atura form section so that it can be rendered multiple times – such as rendering multiple vehicles. (See SectionIsRepeatableAttribute)

Attributes

Attributes determine the rules for how your fields and sections are rendered. They look at the custom field created earlier and take action depending if the object value matches the desired value. There are two types of attributes we are interested in:

  • Field attributes
  • Section attributes

Field Attributes

UsesValueSuppliedBy_InPrompt

One of the more common attributes, used to dynamically render a prompt with a field answered previously in the flow.

If a user answered a question about what type of motor vehicle they have (car, motorbike, truck), your next prompt can include that answer by rendering your field with this attribute.

[UsesValueSuppliedBy_InPrompt(typeof(VehicleTypePromptField))]
public VehicleYearField VehicleYearField { get; set; } = new VehicleYearField();
string VehicleYearPrompt = What year was your {0} manufactured?

UsesValueSuppliedBy_ToFilterList

Allows your field to render its prompt of options depending on a field value that was capture earlier.

If your flow has a list of residence types (Flat, House, Apartment, Town House, Caravan), and you want to remove one, or render a filtered list of those options, you would apply this attribute to your next field.

[UsesValueSuppliedBy_ToFilterList(typeof(ResidenceTypeField))]
public ResidenceOwnerField ResidenceOwnerField { get; set; }

OnlyIfOtherFieldHasValue Allows your field to only be rendered depending on a previous field's captured value.

As you can see from the example below, you would not want to ask the user for the date of their purchase, if they had answered the previous field of PurchasedItem as false.

[OnlyIfOtherFieldHasValue(typeof(PurchasedItem), true)]
public DateOfPurchaseField DateOfPurchase { get; set; } = new DateOfPurchaseField();

OnlyIfXHasValueAndHasOccuredZTimes Allows you to render a field only if an expected value in a previous field was captured and happened a specified number of times If there is a field which needs a count of its rendered times included in the pre-render validation, you can apply this attribute to do the conditional checking.

[OnlyIfXHasValueAndHasOccuredZTimes(typeof(MultipleHomes), "Yes", 2)]
public WhichResidenceField WhichResidence { get; set; } = new WhichResidenceField();

OnlyIfOtherFieldsAreNotNull Allows a field to be rendered only if the previous field has a captured value and is not null or empty. This is a good use case for any fields that might be optional in your flow.

When pre-populating a flow or rendering a prompt based on an optional field's value, you can use this attribute to confirm that the field has been rendered and answered by the user.

[OnlyIfOtherFieldsAreNotNull(new[] { typeof(SuburbField) })]
public StreetAddressField StreetAddress { get; set; } = new StreetAddressField();

OnlyIfOtherFieldsAreNull Allows a field only to be rendered depending on a related previous field having a null or empty value.

When a flow has two sections that can use a similar answer, this attribute will allow you to only render the current field based on a value existing for a previous field.

[OnlyIfOtherFieldsAreNull(new[] { typeof(IDNumberField) })]
public DateOfBirthField DateOfBirth { get; set; } = new DateOfBirthField ()

SkipIfFieldHasValue Allows a field to be skipped or optional based on a previously rendered field's value.

If a field is optional in your flow, this attribute will allow it to be skipped based on an another fields value or captured input.

[SkipIfFieldHasValue(typeof(PurchasePrice), 0)]
public QualifyForDiscountField QualifyForDiscount { get; set; }

Section Attributes

Section attributes are applied when the Atura form data engine renders the current section while instantiating the sections and fields needed for the flow.

Serializable

[Serializable]
  public class BuildingQuote: AturaFormSection
  {
    public static string CoverTypeTitle = "Building";
    public BuildingQuote(): base(CoverTypeTitle)
  {
  }
  public ResidenceTypeField ResidenceType { get; set; }
}

Allows all fields and classes encapsulated on the section to be Serializable.

SectionIsRepeatable Allows the entire section to render all fields contained in it multiple times depending on a hardcoded value or field's dynamic value.

In this example, the SectionIsRepeatable attribute is applied onto a Vehicle quote subsection, allowing the entire section to be rendered again when the user has confirmed they would like to add an additional vehicle to their insurance quote (in the flow).

[SectionIsRepeatable(typeof(AdditionalVehiclePromptField), true)]
public class VehicleQuoteSubsection : AturaFormSubsection
  {
    public VehicleQuoteSubsection() : base("Vehicle")
    {
    }
    public VehicleTypePromptField VehicleTypePromptField { get; set; }

    [UsesValueSuppliedBy_InPrompt(typeof(VehicleTypePromptField))]
    public VehicleYearField VehicleYearField { get; set; }

    [UsesValueSuppliedBy_InPrompt(typeof(VehicleTypePromptField))]
    [UsesValueSuppliedBy_ToFilterTextLookup(typeof(VehicleTypePromptField))]
    public VehicleMakeField VehicleMakeField { get; set; }
}

Functionality

There are five main classes that are responsible for handling the form engine functionality. A brief description and responsibility of each follows below.

  1. AturaAttributeHandler – responsible for rendering and applying attributes as the form engine initialises fields and sections. Includes all attributes mentioned above and utilises the AturaFormElementTracker to determine ordering and rendering.

  2. AturaFieldHandler – handles the field types that are initialized, which rules to apply (depending on field type), and removing sections and fields from the flow once it is complete.

  3. AturaFormDataEngine – initializes the current section and fields, updates the user details model, steps through the fields, interprets users input, hands off to the Form Validator when required and stores status of completed fields.

  4. AturaFormElementTracker – fields and sections are added as elements to the Form Engine. The form element tracker adds, removes and inserts elements in the form engine and keeps track of the index and what order to render the fields on the relevant sections. Handles users going back and rendering of empty or unanswered fields.

  5. AturaFormDataPopulator – shared fields can be prepopulated across sections for the various flows. For example, if a user entered their name in a personal details section, it can then be reused via the Atura Form Data Populator in another section.