Aggiungere un nuovo Scenario - RetiSpA/botler GitHub Wiki

Aggiungere un nuovo Scenario

Per aggiungere un nuovo Scenario, include l'aggiunta di piccole parti di codice in certe classi, oltre alla creazione di dialoghi qualora fosse necessario, ma vediamo i passi necessari.

Creare uno Scenario

Se vogliamo uno scenario il quale ha bisogno di un certo numero di entità allora, creiamo due classi concrete che estendono una DescriptionScenario e un'altra ExecutionScenario.

Vediamo un esempio di uno scenario che usa queste due classi:

OutlookScenario

  • OutlookDescription :
public class OutlookDescriptionScenario : DescriptionScenario
{
    private readonly BotlerAccessors _accessors;
    private readonly ITurnContext _turn;

    public OutlookDescriptionScenario(BotlerAccessors accessors, ITurnContext turn)
    {
        _accessors = accessors ?? throw new ArgumentNullException(nameof(accessors));
        _turn = turn ?? throw new ArgumentNullException(nameof(turn));
    }

    // Uno scenario può avere più intenti
    public override Intent ScenarioIntent { get;  set; }

    public override string ScenarioID { get; set; } = OutlookDescription;
    // Ci serve quando ci sarà bisogno di un cambiare stato
    public override string AssociatedScenario { get; set; } = Outlook;
    // Se è uno scenario che ha bisogno di credenziali Reti per essere eseguito
    public override bool NeedAuthentication { get; set; } = true;
}
  • OutlookExecution :
    public class OutlookScenario : ExecutionScenario
    {
        private readonly BotlerAccessors _accessors;

        private readonly ITurnContext _turn;

        private readonly DialogSet _scenarioDialogs;

        public override string ScenarioID { get; set; } = Outlook;

        public override bool NeedAuthentication { get ; set; } = true;

        public override Intent ScenarioIntent { get; set; }

        public override string AssociatedScenario { get; set; } = OutlookDescription;

        public OutlookScenario(BotlerAccessors accessors, ITurnContext turn)
        {
            _accessors = accessors ?? throw new ArgumentNullException(nameof(accessors));
            _turn = turn ?? throw new ArgumentNullException(nameof(accessors));

            _scenarioDialogs = new DialogSet(accessors.DialogStateAccessor);
        }

        public async Task<DialogContext> GetDialogContextAsync()
        {
            DialogContext currentDialogContext = await _scenarioDialogs.CreateContextAsync(_turn);
            return currentDialogContext;
        }

        public override async Task CreateResponseAsync(LuisServiceResult luisServiceResult)
        {
             // Aggiungiamo tutti i dialoghi dello Scenario
            _scenarioDialogs.Add(new LetturaMailOutlook(_accessors, ScenarioIntent));
            _scenarioDialogs.Add(new PrenotaSalaRiunioni(ScenarioIntent));
            _scenarioDialogs.Add(new CreaAppuntamentoCalendar(ScenarioIntent, _accessors));
            _scenarioDialogs.Add(new VisualizzaAppuntamentiCalendar(_accessors, ScenarioIntent));

            DialogContext currentDialogContext = await GetDialogContextAsync();
            
            DialogTurnResult dialogResult = null;

            dialogResult = await currentDialogContext.ContinueDialogAsync();
            
            // verifichiamo lo stato della conversazione
            switch (dialogResult.Status)
            {
                    case DialogTurnStatus.Empty:
                    {
                        await currentDialogContext.BeginDialogAsync(ScenarioIntent.DialogID);
                        break;
                    }

                    case DialogTurnStatus.Waiting:
                        break;

                    case DialogTurnStatus.Complete:
                    {
                        await currentDialogContext.EndDialogAsync();
                        break;
                    }

                    default:
                    {
                        await currentDialogContext.CancelAllDialogsAsync();
                        break;
                    }
             }

    }

Ovviamente queste due classi potranno cambiare all'esigenza, sono solo linee guida.

Intent di uno Scenario

Ora bisogna però creare gli Intent di uno scenario, mappando gli intenti inseriti in LUIS per quello scenario nel codice del Bot.

E' molto semplice in realtà, basterà fare qualcosa modifica il file Enviroment.cs il quale contiene una serie di classi statiche con stringhe constanti e Strutture Dati statiche.

  1. Aggiungiamo la stringa con il nome dell'intento su LUIS nella classe
  public static class LuisIntent
    {
        public const string VisualizzaAppuntamentiCalendarIntent = "VisualizzaAppuntamentiCalendar";
        public const string CreaAppuntamentoCalendarIntent = "CreaAppuntamentoCalendar";
        ...
    }
  1. Aggiugiamo gli intenti che fanno parte di uno scenario nella classe:
    public static class IntentsSets
    {

        public static HashSet<string> OutlookIntents = new HashSet<string> () { LeggiMailIntent, 
             PrenotazioneSalaRiunioniIntent, VisualizzaAppuntamentiCalendarIntent, CreaAppuntamentoCalendarIntent}; 
    }

Questo HashSet ci sarà utile quando controlleremo se un intent provveniente dalla classe LuisServiceResult fa parte uno scenario specifico:

       private static bool isAOutlookIntent(LuisServiceResult luisServiceResult)
        {
            var topIntent = luisServiceResult.TopScoringIntent.Item1; // intent
            var score = luisServiceResult.TopScoringIntent.Item2; // score

            return (OutlookIntents.Contains(topIntent) && (score >= 0.75));
        }

Questo metodo va aggiunto nella classe ScenarioRecognizer descritta nella pagina precedente.

Per i Dialoghi vedere qualche esempio di classe già implementata nella cartella Botler\Dialogs\Dialoghi

Creare un Intent

Per creare effettivamente un istanza di Intent che descrive le caratteristiche e le necessità di un LUIS intent, creiamo una classe che implementa l'interfaccia:

    public interface IIntentBuilder
    {
        Intent BuildIntent(LuisServiceResult luisServiceResult);
    }

Come segue da questo esempio:

    public class LeggiMailIntentBuilder : IIntentBuilder
    {
        public Intent BuildIntent(LuisServiceResult luisServiceResult)
        {
            Intent intent = new GenericIntentBuilder().BuildIntent(luisServiceResult);

            intent.NeedEntities = true;
            intent.EntityLimit = 2;
            intent.EntityLowerBound = 1;
            intent.DialogID = nameof(LetturaMailOutlook);
            intent.EntityNeedResponse = LeggiMailEntityNeedsToCollect;

            return intent;
        }
    }

Ricordadosi di aggiungere un controllo nella Factory degli Intent:

    public static class IntentFactory
    {
        public static Intent FactoryMethod(LuisServiceResult luisServiceResult)
        {
            var intent = luisServiceResult.TopScoringIntent.Item1;
            var score = luisServiceResult.TopScoringIntent.Item2;

            if (intent.Equals(LeggiMailIntent))
            {
                return new LeggiMailIntentBuilder().BuildIntent(luisServiceResult);
            }

            ...
            // Intento generico che non ha bisogno di informazioni aggiuntive (SalutoIntent e.g)
            return new GenericIntentBuilder().BuildIntent(luisServiceResult);
        }
    }

Entità aggiuntive

Potremo avere bisogno di dovere aggiungere delle entità che non esistono, oltre che crearle in LUIS, molto simile a quanto fatto per gli Intent, ma con meno passaggi.

  1. Aggiungere l'entità nella classe LuisEntity nel file Enviroment.cs
  2. Aggiungere l'entità negli EntitySets relativi agli Intent:
public static HashSet<string> OutlookEntities = new HashSet<string>() { Datetime, MailUnread, DatetimeRegex, 
              DatetimeBuiltin, SalaRiunioni, Appuntamento, SaleRiunioni, Time, TimeRegex, Email };

Vedere la classe EntityFormatHelper per ulteriori dettagli, e aggiungere funzioni qualora servisse.

Le entità vengono lette dal JSON provveniente da LUIS e deserializzate in classi Entity, percui qualche volta è necessario fare qualche passaggio aggiuntivo per avere un instanza completa e significativa, come per esempio accade con le stringhe di date testuali ( 24 giugno e.g) qui entrano in gioco Regex e funzioni relative alle Regex.

Filtro delle entità dato un intento

Questa parte è fondamentale per evitare duplicati, cosa già evitata creando come tipo effettivo delle ICollection presenti in Intent e quindi negli Scenari, come HashSet, ma potremmo trovarci comunque in situazioni dove l'utente inserisce entità che non servono per l'intento che vuole svolgere e quindi non andrebbero considerate, quindi ogni dialogo e ogni scenario Description usa questo metodo per evitare entità 'inutili'

        public static ICollection<Entity> EntityFilterByIntent(string intentName, ICollection<Entity> entitiesCollected)
        {
            ISet<Entity> set = new HashSet<Entity>();

            foreach (var ent in entitiesCollected)
            {
                if (OutlookIntents.Contains(intentName) && OutlookEntities.Contains(ent.Type))
                {
                    set.Add(ent);
                }

                if (ParkingIntents.Contains(intentName) && ParkingEntities.Contains(ent.Type))
                {
                    set.Add(ent);
                }

                if (SupportIntents.Contains(intentName) && (SupportEntities.Contains(ent.Text) || SupportEntities.Contains(ent.Type)))
                {
                    set.Add(ent);
                }
            }

            return set;
        }
⚠️ **GitHub.com Fallback** ⚠️