¡Lo prometido es deuda! Y es que comenté hace un par de meses que hablaría con algo más de detalle de la segunda parte de la sesión de Alberto Marcos y servidor en la Global Azure Bootcamp 2019 de Madrid.

En esta segunda parte, dotábamos a los bots cierta sensibilidad para saber el estado de ánimo de los jugadores del MMORPG a través del uso del servicio de Azure Text Analytics, parte de los Cognitive Services.

La situación es que un bot que se mueve como un jugador más y escucha conversaciones públicas de otros jugadores al alcance de su oído, puede analizar estas frases y en función del estado de ánimo tomar decisiones. Un diagrama de flujo sencillo de la situación sería el siguiente:

¿Cómo podemos conseguir esto? Gracias a la potente arquitectura de plugins de OpenKore que ya mencionamos en pasados artículos. Así pues, siguiendo el diagrama:

  1. Un jugador hace un comentario público.
  2. El bot lo escucha y a través de un hook le envía la información al plugin, que debe estar desarrollado en Perl.
  3. El plugin lanza una petición HTTP a una Azure LogicApp que incluye la frase que ha escuchado.
  4. La LogicApp envía a su vez esa frase Azure Text Analytics para hacer un Sentiment Analysis, que básicamente nos va a dar un valor entre 0 y 1, donde valores cercanos a 0 indican que está hablando negativamente y 1 positivamente.
  5. El valor que se obtenga de resultado devuelto a la LogicApp, que realiza dos acciones, siendo la primera el almacenar el resultado en una Azure Storage Table para propósitos de análisis posterior...
  6. ... y la segunda devuelve una respuesta a la petición HTTP que el bot hizo en primer lugar. En este caso la respuesta es el coeficiente de sentiment en plano, sin formato.
  7. En base a ese valor devuelto por HTTP, el bot toma un curso de acción determinado.

Más allá de la prueba conceptual que aquí planteamos, ¿os imagináis las posibilidades que este experimento implica? Podríamos, por ejemplo, tener en un MMORPG un ejército de personajes invisibles puedan analizar conversaciones y detectar situaciones sospechosas de ser bullying o acoso para que sean investigadas de oficio, sin necesidad de ser denunciadas. O bien hacer estadísticas del estado de ánimo general de los jugadores.

Talk is cheap, show me the code

El código se encuentra publicado en este repositorio de Github, concretamente en la carpeta plugin donde encontramos un archivo llamado AzureCognitive.pl.

La sección del código que nos interesa es la función inbound_pubMsg() que se va a ejecutar cada vez que nuestro bot escucha una conversación pública, de la misma manera que podríamos escuchar alguna conversación mientras vamos andando por la calle. El código sería el siguiente:

sub inbound_pubMsg {
    my (undef, $args) = @_;
	my $charname;
    my $chatmsg;
	my $actor;
    my $laclient = REST::Client->new();
    my $reporterName = $ENV{'STORAGE_QUEUE_NAME'};
    my $postJson;
    my $rnd;

    message("[azureCognitive] inbound_pubMsg was successfully called\n");
	if (defined $args->{pubMsgUser}) {
        $charname = $args->{pubMsgUser};
        $chatmsg = $args->{Msg};
		$actor = Actor::get($args->{pubID});
		if ($actor->{guild}{name} ne '') { return; }
	}

    if(index($charname, 'botijo') < 0 ) {
        $rnd = rand();
        message("[azureCognitive] I have heard a chat message from $chatmsg. Random is $rnd\n");
        if ($rnd > 0.9)
        {
            message('[azureCognitive] Randomness decided to go for processing!');
            $laclient->setHost("https://prod-59.westeurope.logic.azure.com");
            $laclient->addHeader('Content-Type', 'application/json');
            $laclient->setTimeout(10);
            $postJson="{ \"reporterName\": \"$reporterName\", \"playerName\": \"$charname\", \"text\": \"$chatmsg\" }";
            $laclient->request('POST', '/workflows/[REPLACE WITH YOUR WORKFLOW ID]/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=[REPLACE WITH YOUR SIGNATURE]', $postJson);

            if($laclient->responseCode() eq '201') {
                Commands::run("c Ey $charname, he enviado a Azure Cognitive lo que has dicho para que sea analizado");
                sleep 2;
                my $sentiment = $laclient->responseContent();
                Commands::run("c Tus palabras denotan un coeficiente de felicidad de $sentiment");
                sleep 2;
                if ($sentiment < 0.3) {
                    Commands::run("c Parece que estas enfadado");
                    sleep 2;
                    Commands::run("e lv");
                    Commands::run("e lv");
                    sleep 2;
                    Commands::run("c No me voy a despegar de ti hasta que sonrias");
                    sleep 2;
                    Commands::run("e lv");
                    Commands::run("follow $charname");
                } else {
                    if ($sentiment >= 0.3 && $sentiment < 0.7) {
                        Commands::run("c Parece que estas ni fu ni fa");
                        sleep 2;
                        Commands::run("e hmm");
                    } else {
                        Commands::run("c Parece que estas muy contento");
                        sleep 2;
                        Commands::run("e 21");
                        sleep 2;
                        Commands::run("c Hakunamatata Hulio");
                        Commands::run("e no1");
                        Commands::run("follow stop");
                    }
                }
            } else {
                Commands::run("c Humm... $charname, he intentado enviar lo que has comentado a Azure Cognitive...");
                sleep 2;
                Commands::run("c pero no se Rick, parece que no funciona");
                sleep 2;
                Commands::run("c " . $laclient->responseCode());
                Commands::run("c " . $laclient->responseContent());
            }
        }
    }
}

Cada vez que escucha un mensaje público, el bot hace lo siguiente:

  1. Descartamos que la conversación escuchada sea de un bot, sólo queremos analizar el texto de los humanos :)
  2. Genera un número aleatorio entre 0 y 1. Esto lo hacemos para que no necesariamente reaccione a todas las frases.
  3. Si el número aleatorio es superior a 0.9, analizamos la frase. Esto quiere decir que el bot va a actuar un 10% de las veces que escuche algo.
  4. Lanzamos una petición HTTP REST a la Azure LogicApp con la siguiente información: qué bot es el que está informando, qué jugador dijo la frase y la frase en cuestión; otros datos como la fecha y hora van implícitos en la petición.
  5. Si la respuesta es un HTTP 201 significa que el análisis ha ido bien y en función del coeficiente hacemos distintas cosas:
    • Si menos que 0.3, el bot decide seguirnos a donde quiera que vayamos.
    • Si está entre 0.3 y 0.7, el bot te ignora.
    • Si es superior a 0.7 y previamente nos estaba siguiendo, deja de hacerlo.

Vale pero... ¿y el código de la Logic App?

Las Azure LogicApp se construyen de forma declarativa y con una interfaz gráfica que permite desarrollar el flujo muy intuitivamente. Aún así se puede exportar a formato JSON y mostrar cómo está construida, por lo que la adjunto a continuación:

{
    "$connections": {
        "value": {
            "azuretables": {
                "connectionId": "/subscriptions/[replace this with Azure Subscription ID]/resourceGroups/gab19/providers/Microsoft.Web/connections/azuretables",
                "connectionName": "azuretables",
                "id": "/subscriptions/[replace this with Azure Subscription ID]/providers/Microsoft.Web/locations/westeurope/managedApis/azuretables"
            },
            "cognitiveservicestextanalytics": {
                "connectionId": "/subscriptions/[replace this with Azure Subscription ID]/resourceGroups/gab19/providers/Microsoft.Web/connections/cognitiveservicestextanalytics",
                "connectionName": "cognitiveservicestextanalytics",
                "id": "/subscriptions/[replace this with Azure Subscription ID]/providers/Microsoft.Web/locations/westeurope/managedApis/cognitiveservicestextanalytics"
            }
        }
    },
    "definition": {
        "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
        "actions": {
            "Detect_Sentiment": {
                "inputs": {
                    "body": {
                        "text": "@triggerBody()['text']"
                    },
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['cognitiveservicestextanalytics']['connectionId']"
                        }
                    },
                    "method": "post",
                    "path": "/sentiment"
                },
                "runAfter": {},
                "type": "ApiConnection"
            },
            "Insert_Entity": {
                "inputs": {
                    "body": {
                        "PartitionKey": "@{triggerBody()['reporterName']}",
                        "PlayerName": "@{triggerBody()['playerName']}",
                        "RowKey": "@{body('Detect_Sentiment')?['id']}",
                        "Sentiment": "@{body('Detect_Sentiment')?['score']}",
                        "Text": "@{triggerBody()['text']}"
                    },
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['azuretables']['connectionId']"
                        }
                    },
                    "method": "post",
                    "path": "/Tables/@{encodeURIComponent('gab19sentiment')}/entities"
                },
                "runAfter": {
                    "Detect_Sentiment": [
                        "Succeeded"
                    ]
                },
                "type": "ApiConnection"
            },
            "Response": {
                "inputs": {
                    "body": "@{body('Detect_Sentiment')?['score']}\n",
                    "headers": "@triggerOutputs()['headers']",
                    "statusCode": 201
                },
                "kind": "Http",
                "runAfter": {
                    "Insert_Entity": [
                        "Succeeded"
                    ]
                },
                "type": "Response"
            }
        },
        "contentVersion": "1.0.0.0",
        "outputs": {},
        "parameters": {
            "$connections": {
                "defaultValue": {},
                "type": "Object"
            }
        },
        "triggers": {
            "manual": {
                "inputs": {
                    "method": "POST",
                    "schema": {
                        "$schema": "http://json-schema.org/draft-04/schema",
                        "definitions": {},
                        "properties": {
                            "playerName": {
                                "type": "string"
                            },
                            "reporterName": {
                                "type": "string"
                            },
                            "text": {
                                "type": "string"
                            }
                        },
                        "required": [
                            "reporterName",
                            "playerName",
                            "text"
                        ],
                        "type": "object"
                    }
                },
                "kind": "Http",
                "type": "Request"
            }
        }
    }
}

Podéis ver el bot con el plugin en acción y un vistazo a la parte gráfica de la LogicApp en esta sección del video:

Y eso ha sido todo por ahora. Como mencionaba al principio del artículo, no sólo tenemos ante nosotros un experimento simpático, sino que abre todo un mundo de posibilidades de cara a lo que un mundo online persistente puede hacer con las capacidades y servicios de Azure.

Happy cognitive!