Search results:
No results: try another search
Tech
Raccourcis innovatifs, conseils et outils pour améliorer son workflow.

Retour d’expérience sur le ...

Cela est notamment dû aux avancées relativement récentes dans le domaine du Traitement du langage naturel (Natual Language Processing ou NLP), et au fait que l’interface texte est très courante (et très accessible) sur mobile. Notre laboratoire d’innovation Marmelab a décidé d’explorer cette technologie et pour ce faire, de réaliser un projet concret nommé Tobaccobot. Il s’agit d’un coach virtuel pour arrêter de fumer en un mois, avec qui l’utilisateur communique uniquement par SMS. L’interface Le principe du bot est très simple: une personne qui a envie d’arrêter de fumer s’inscrit au programme via une page web, avec son nom et son numéro de téléphone. A partir de là, toutes les interactions se font par SMS. Le fumeur reçoit un message qui lui demande combien de cigarettes il a fumé ce jour-là. A partir de la réponse à cette question, le bot va déterminer un nombre maximum de cigarettes à ne pas dépasser pour la prochaine semaine – avec pour objectif d’aider le fumeur à arrêter totalement en 4 semaines. Chaque matin, le fumeur recevra un SMS lui demandant combien de cigarette il a fumé la veille, de manière à évaluer la progression. En fonction de sa réponse, le bot l’encouragera ou le réprimandera. Et les réponses devront varier d’un jour sur l’autre, pour ne pas lasser le fumeur. A la fin de chaque semaine, le bot fixe un nouvel objectif à atteindre – forcément plus ambitieux que la semaine précédente. A la fin de la 4ème semaine, le bot détermine si le fumeur est oui ou non parvenu à arrêter de fumer. Il envoie un message d’adieu et la conversation s’arrête là. A tout moment, le fumeur peut décider d’interrompre le programme. Note:Nous ne sommes pas tabacologues chez Marmelab – et pour tout dire, il n’y a même pas de gros fumeur chez nous. Ce cas d’utilisation a juste été choisi pour servir de support à une expérimentation technique. Si ce coach virtuel aide un jour quelqu’un à arrêter de fumer, alors nous aurons fait d’une pierre deux coups ! Le workflow de conversation Pas évident de trouver un formalisme pour modéliser une interface conversationnelle. Nous avons tenté de dessiner des boites et des flèches, et nous sommes parvenus au résultat suivant : Note:Après le début du développement, nous avons découvert un super outil pour modéliser un workflow à partir d’une description texte :code2flow. Les technologies utilisées Pour implémenter ce coach virtuel par SMS, nous avons choisi d’utiliser les technologies suivantes: Node.js pour la partie serveur, en mode Serverless avec AWS lambda DynamoDb pour le stockage de l’état du fumeur Octopush pour l’envoi et la réception de SMS nlp compromise pour le traitement en langage naturel (ou Natural Language Processing, NLP) Nous allons revenir en détail sur les raisons du choix de ces technologies et leur utilisation dans les sections suivantes. Si vous voulez voir du code, sautez à la fin de l’article pour y trouver le lien vers la source du projet, que nous publions en licence MIT. Comment ça ?! Pas de botkit ! Dans le monde Node.js, la librairie de référence pour implémenter des chatbots est Botkit. Cette librairie très populaire, bien qu’étant d’excellente qualité, ne correspond pas à notre cas d’utilisation. Tout d’abord, botkit vise surtout les plateformes de chat (Slack, Messenger, etc.) mais ne supporte pas l’envoi et la réception de SMS. Il existe bien botkit-sms, mais ce projet n’est pas très actif, et utilise Twilio. Or nous avons choisi Octopush. Il aurait donc fallu développer notre propre adaptateur. Ensuite, Botkit est prévu pour écouter sur un port l’arrivée de messages. Il s’agit d’un démon, un process node qui ne s’arrête jamais. Mais avec serverless, le service doit s’arrêter après le traitement de chaque message, et est stoppé de force s’il ne rend pas la main dans les 5 secondes. Il aurait donc fallu forcer botkit à quitter après chaque message en killant le process node – pas très propre. Enfin, puisqu’il est prévu pour s’exécuter sous forme de tâche de fond, botkit persiste le contexte des conversations en mémoire. Ce contexte est vidé à chaque redémarrage. Donc nativement, il n’est pas facile de conserver un contexte de conversation en mode serverless. Il est bien entendu possible de fournir à botkit un stockage de conversation personnalisé (pour sauvegarder vers dynamodDb dans notre cas). Mais botkit impose trois tables: users, channels et teams, dont au moins deux n’ont aucun sens dans notre cas (channels et teams). Il aurait fallu tout de même les implémenter ou du moins les mocker. Au vu de toutes ces limitations, nous avons décidé que botkit n’était pas approprié pour notre application. AWS Lambda Vous connaissez peut-être le principe d’AWS lambda: c’est un hébergement PaaS (platform-as-a-service) semblable à heroku, où on ne déploie que… des fonctions. Dans ce contexte, une application est un ensemble de fonctions qui sont appelées en réponse à des événements (par exemple requête HTTP ou cron). Et c’est API Gateway, autre service d’Amazon, qui se charge de router les appels à une API HTTP vers une fonction lambda pour en calculer la réponse. Cela permet de n’exécuter le code que quand il est nécessaire, et de se passer d’un serveur web. L’activité de notre bot est très ponctuelle : il relance l’utilisateur une fois par jour, et n’attend qu’une réponse par jour. Faire tourner un serveur pour rien 99% du temps serait du gâchis dans ce cas; l’approche AWS lambda est toute indiquée. Sous le capot, AWS utilise Docker pour stocker les fonctions lambda. Il réveille un conteneur lorsqu’une lambda est sollicitée, et le rendort après quelques minutes d’inactivité. Mais tout cela se fait automatiquement, et le développeur ne voit, lui, que des fonctions. Donc hormis le “serveur” d’API Gateway, qui est en fait juste un reverse proxy géant mutualisé, AWS facture uniquement l’hébergement lambda à l’appel de fonction, c’est-à-dire à la requête. Et c’est extrêmement bon marché (le premier million de requêtes est gratuit). Serverless Serverless est une librairie JS open-source qui permet d’utiliser AWS lambda facilement, en automatisant la configuration et le déploiement sur AWS. Cette librairie prend en charge non seulement AWS lambda bien sûr, mais aussi API Gateway pour les événements HTTP, ainsi que cron et Dynamodb pour la base de donnée dans notre cas. Serverless utilise un fichier de configuration serverless.yml, dans lequel on déclare les lambdas (functions) et les resources (resources) utilisées par les lambdas. Voici pour exemple celui de tobaccobot : service: tobaccobot functions: botConversation: handler:src/serverless/index.botConversation # la fonction exportée avec le nom botConversation dans le fichier index.js events: # ce qui déclenche l'appel de cette fonction - http: # la partie HTTP sert à configurer API Gateway method: POST integration: lambda path: bot_conversation # le path dans l'url cors: true # L'API HTTP accepte les appels de n'importe quel domaine (CORS) getBotConversation: handler: src/serverless/index.botConversation events: - http: method: GET # la même fonction doit répondre en POST et en GET, contrainte d'octopush (voir plus loin) integration: lambda path: bot_conversation cors: true dailyMessage: handler: src/serverless/index.dailyMessage events: - schedule: # ici ce n'est pas une requête HTTP qui déclenche l'appel mais un cron rate: cron(0 8 ? * * *) enabled: true setupTables: handler: src/serverless/index.setupTables # pas d'events, on ne peut donc l'appeler qu'avec l'API AWS subscribe: handler: src/serverless/index.subscribe events: - http: method: POST integration: lambda path: subscribe cors: true reportData: handler: src/serverless/index.reportData events: - http: method: POST integration: lambda path: report_data cors: true resources: Resources: # une table dynamodb pour stocker les infos des fumeurs DynamoDbSmokerTable: # Les noms de ressources doivent être uniques Type: AWS::DynamoDB::Table Properties: TableName: smoker AttributeDefinitions: - AttributeName: phone AttributeType: S # string KeySchema: - AttributeName: phone KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 # une policy IAM pour permettre aux lambdas d'accéder à cette table dynamodb DynamoDBSmokerIamPolicy: # Y compris les noms des policies Type: AWS::IAM::Policy DependsOn: DynamoDbSmokerTable Properties: PolicyName: lambda-dynamodb-smoker # Ce nom doit également être unique PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - dynamodb:DescribeTable - dynamodb:GetItem - dynamodb:PutItem - dynamodb:UpdateItem - dynamodb:DeleteItem - dynamodb:Scan Resource: arn:aws:dynamodb:*:*:table/smoker Roles: - Ref: IamRoleLambdaExecution # une autre table dynamodb pour stocker les infos des fumeurs qui sont arrivés au bout du programme DynamoDbArchiveTable: Type: AWS::DynamoDB::Table Properties: TableName: archive AttributeDefinitions: - AttributeName: id AttributeType: S KeySchema: - AttributeName: id KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 # comme la précédente, il faut une policy pour la rendre accessible DynamoDBArchiveIamPolicy: Type: AWS::IAM::Policy DependsOn: DynamoDbArchiveTable Properties: PolicyName: lambda-dynamodb-archive PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - dynamodb:DescribeTable - dynamodb:GetItem - dynamodb:PutItem - dynamodb:UpdateItem - dynamodb:DeleteItem - dynamodb:Scan Resource: arn:aws:dynamodb:*:*:table/archive Roles: - Ref: IamRoleLambdaExecution provider: name: aws runtime: nodejs4.3 stage: dev region: eu-west-1 cfLogs: true plugins: - serverless-webpack - serverless-offline custom: webpack: ./webpack.config.serverless.js # notre conf webpack serverless-offline: # la conf pour l'exécution en local babelOptions: presets: ["es2015-node4", "es2016"] plugins: ["add-module-exports", "transform-runtime"] Serverless fournit sa propre version du package aws-sdk, déjà configuré avec les bons accès. Et cela inclut les accès IAM. Serverless : les pièges Le hic principal avec serverless, c’est l’environnement de développement. Un développeur n’a pas de service AWS qui tourne sur son poste de travail. Comment tester les fonctions lambda dans ce contexte ? Le plugin serverless-webpack permet de servir les lambdas en local, mais il ne suit pas la spécification API gateway. Heureusement, il existe le plugin serverless-offline, qui émule AWS lambda et API gateway. Il accepte aussi une configuration babel. C’est un must have ! Serverless a eu une mise à jour majeure entre les versions 0.5 et 1.0, et l’on trouve encore beaucoup de documentation concernant la version précédente. Ne soyez pas étonné que le copier/coller depuis Stack Overflow ne donne rien, et lisez la doc officielle. Les logs des lambdas sont consultables grâce à la commande serverless logs -f [lambdaName]. Peu importe le nombre de conteners utilisés par AWS: tous les logs d’une lambda sont rassemblés chronologiquement. Serverless consigne automatiquement le résultat des console.error() et console.info(), mais il ignore les console.log(). API Gateway ne peut retourner que du JSON. Il est donc impossible d’utiliser une lambda pour servir du HTML, ou une image qui serait générée. Node.js Pour ce qui est du code en lui-même, AWS lambda utilise Node 4.3.2. Serverless compresse le code de la fonction lambda dans un fichier zip. Les packages node ne sont pas inclus, et AWS lambda n’accepte pas d’en installer de son côté. Pour utiliser des packages externes, il faut donc concaténer notre code et celui de ses dépendances dans une seule fonction – c’est le travail d’un module bundler. Nous avons choisi Webpack, que nous utilisons couramment pour le développement frontend. Serverless fournit également un plugin webpack pour automatiser la construction des fichiers à déployer: plugins: - serverless-webpack custom: webpack: chemin/vers/webpack.config.js D’ailleurs, quitte à utiliser webpack, autant ajouter babel également, histoire de profiter des dernières nouveautés d’ES6. Rien de nouveau de ce côté là. Un inconvénient de webpack est que certaines librairies que l’on a l’habitude d’utiliser côté serveur ne fonctionnent plus. C’est par exemple le cas de config, qui lit les fichiers de config au moment de l’exécution. Ce problème est nuancé par la disponibilité d’un plugin permettant de reproduire le mécanisme de manière transparente: webpack-config-plugin. DynamoDb DynamoDb est une base de donnée clef/valeur relativement simple, semblable à Redis. Elle permet de définir un table avec une clef de partition qui sert d’identifiant unique pour l’objet. Si on veut, on peut ajouter une clef de tri, mais dans ce cas la clef de partition n’est plus unique et c’est la clef de tri qui fait la différence. Dans notre cas nous avons choisi une seule clef de partition: le numéro de téléphone de l’utilisateur. Mis à part les clefs, un document dynamoDb n’a aucune validation et accepte tout format. Aws-sdk fournit l’objet dynamoDb pour interroger le service dynamoDb. Il propose également une interface web très facile d’utilisation. Dynamo DB : les pièges DynamoDb retourne des objets avec une structure un peu particulière, qui précise le type de chaque champ: { name: { S: 'john' // une clef est ajouté pour préciser le type de donnée de l'attribut, ici une string } } Il est fastidieux de convertir ce format en simple format JSON et inversement. Heureusement, il existe la librairie dynamodb-oop qui réalise cette transformation et offre une api légèrement plus agréable. Il faut néanmoins faire attention à 2 points : Une opération getItem retourne un objet vide ({}) et non null lorsque l’objet n’a pas été trouvé. Les opérations createTable et deleteTable, bien qu’acceptant un callback, retournent lorsque l’opération à été initialisée et non pas terminée. Pour être sûr que ce type d’opération est achevée, il faut utiliser dynamloDb.waitFor qui permet d’attendre un événement, en l’occurrence tableExists et tableNotExists. Par exemple pour createTable : function createTable(params) { return new Promise((resolve, reject) => { dynamoDB.on('error', (operation, error) => reject(error)); dynamoDB.client.createTable(params, (err) => { if (err) { reject(err); return; } dynamoDB.client.waitFor('tableExists', params, (errTableExists, result) => { if (errTableExists) return reject(errTableExists); return resolve(result); }); }); }); } A noter que côté AWS, serverless gère la création de la table automatiquement. Pour émuler le stockage sur dynamo DB en local, il existe un module dynamodb-local. Il n’offre par contre pas d’interface web pour consulter et éditer le contenu dynamodb aisément. dynamodb-local ne propose qu’une console beaucoup trop limitée, puisqu’elle demande de coder les opérations à réaliser en javascript en utilisant aws-sdk. Cette console est accessible sur le port 8000. Octopush Pour l’envoi des SMS nous avons choisi Octopush qui est le moins cher, malgré une api orientée campagne de publicité. Pour utiliser Octopush il existe un module node: octopush. L’utilisation est très simple: // On crée une instance de SMS avec nos credentials const sms = new octopush.SMS(config.octopush.user_login, config.octopush.api_key); // On appelle ensuite un certain nombre de fonctions de configuration, par exemple: sms.set_sms_text(message); sms.set_sms_recipients([phone]); // Attention, il faut passer un tableau sms.set_sms_request_id(sms.uniqid()); // Il est possible de spécifier un identifiant que l'on génère de notre côté ... // L'envoi du sms sms.send((error, result) => { ... }); Il est à noter qu’Octopush supporte le publipostage comme le suggère le fait que set_sms_recipients accepte un tableau de numéros de téléphones. Il est alors possible de remplacer des variables dans le texte. Malheureusement, elles ne sont qu’au nombre de 5: {ch1}, les valeurs sont spécifiées en appelant sms.set_sms_fields_1([…]) {ch2}, les valeurs sont spécifiées en appelant sms.set_sms_fields_2([…]) {ch3}, les valeurs sont spécifiées en appelant sms.set_sms_fields_3([…]) {prenom}, les valeurs sont spécifiées en appelant sms.set_recipients_first_names([…]) {nom}, les valeurs sont spécifiées en appelant sms.set_recipients_last_names([…]) Octopush : les pièges Pour gérer les réponses des utilisateurs, il faut fournir une URL qu’Octopush appellera avec la réponse. Pour se conformer aux spécifications d’Octopush, cette URL doit répondre immédiatement sans retourner de contenu. Le traitement par l’application cliente doit donc s’effectuer de manière asynchrone après avoir répondu à Octopush. Octopush demande également que cette url soit interrogeable en GET pour pouvoir la tester depuis un navigateur…. La vérification de cette url n’est pas automatisée pour l’instant, et peut leur prendre jusqu’à une journée… Octopush ne récupère que les SMS en réponse à un message attendant une réponse (option_with_replies). Cela signifie que si l’utilisateur envoie plusieurs messages successifs, seul le premier sera pris en compte. Nous avons eu besoin d’une quatrième variable pour l’un de nos messages et avons simplement utilisé la variable prenom dans ce cas. Au moment de l’écriture de cet article, la documentation d’octopush précise à tort que set_recipients_first_names remplacera les chaines {nom} et que set_recipients_last_names remplacera les chaines {prenom}. Tobaccobot en détail La logique de conversation Le workflow de conversation montre que ce bot est en fait une machine à état tout-à-fait classique. Une action (une requête HTTP, un cron) fait passer l’objet smoker d’un état à un autre en fonction de certaines règles. Il existe de nombreuses librairies pour implémenter une machine à état, mais vu la simplicité de la logique de tobaccobot, nul besoin d’aller chercher plus loin que quelques if imbriqués dans une fonction. La signature de cette fonction est (etat, action) => état. Si vous pratiquez la programmation fonctionnelle ou React, vous reconnaissez sans doute ce pattern: c’est celui d’un reducer. Et une librairie a fait beaucoup parler d’elle pour une implémentation de ce pattern à destination de React: c’est redux. Utilisant cette librairie de façon intensive sur des projets frontend, nous avons naturellement commencé par elle pour implémenter la logique de conversation. Mais en définitive, redux n’apporte rien de plus que la fonction reduce() native dans notre cas, et nous avons fini par supprimer cette dépendance. Voici par exemple un extrait du code qui, à partir de l’état du smoker déduit d’un nombre de cigarettes consommées, déduit le message à envoyer: export default (evaluation) => { if (evaluation.backFromBad === 1) { return backFromBad(); } if (evaluation.backFromBad === 2) { return backFromReallyBad(evaluation.targetConsumption); } if (evaluation.backFromBad > 2) { return backFromBadCombo(); } const lastDelta = evaluation.delta.slice(-1)[0]; const previousDelta = evaluation.delta.slice(-2)[0]; if (lastDelta <= -3) { if (evaluation.delta.length >= 2 && previousDelta <= -3) { return continuedGreatProgress(lastDelta); } return greatProgress(lastDelta); } if (evaluation.state === 'bad') { if (evaluation.combo.hit === 2) { return reallyBad(reallyBadLinks[(evaluation.combo.repeatition - 1) % 3]); } if (evaluation.combo.hit > 2) { return badCombo( evaluation.combo.hit, evaluation.targetConsumption, badComboLinks[(evaluation.combo.repeatition - 1) % 3] ); } return bad(evaluation.targetConsumption); } if (evaluation.combo.hit === 2) { return reallyGood(); } if (evaluation.combo.hit > 2) { return goodCombo(evaluation.combo.hit); } return good(); }; Pour le contenu des messagesbackFromBad(),backFromReallyBad()et les autres, jetez un oeil àla source. Les side effects Dans notre machine a état, les actions ont deux effets: changer l’état du smoker, et un ensemble d’opérations qui ne sont pas répercutées dans l’état du smoker (stockage dans dynamodb, envoi de SMS, logs). Cet ensemble d’opérations n’est pas modélisable par une fonction pure (au sens de la programmation fonctionnelle), on les appelle des side effects. Très souvent, ces side effects sont des opérations asynchrones. Pour gérer ces opérations asynchrones, plutôt que d’utiliser les callbacks, nous avons utilisé les générateurs. Et nous nous sommes aidés de sg, une petite librairie créé par Marmelab. sg gère l’ordonnancement des tâches asynchrones avec des générateurs (comme le fait co.js), mais au lieu de retourner des promesses directement, sg retourne des effets décrivant quoi faire (comme le fait redux-saga). Les générateurs permettent de décrire le flux des actions asynchrones de manière synchrone et, avec les effets, on peut testerl’ordonnancement des opérations sans avoir à ce soucier de leurs implémentations. L’effet le plus couramment utilisé est call. Il s’agit simplement de l’appel d’une fonction asynchrone. Par exemple, avec le générateur suivant: export default function* dailyMessageSaga(smokers) { const dailySmokers = yield call(getDailySmokers, smokers); const { asked = [], dubious = [], qualified = [] } = yield call(sortSmokersByState, dailySmokers); yield call(notifyDubious, dubious); // Users with asked state haven't answered the previous day, we send them a message for the current day anyway yield call(notifyQualified, [...asked, ...qualified]); } Il est possible d’écrire les tests de cette façon: describe('dailyMessageSaga', () => { let iterator; before(() => { iterator = dailyMessageSaga('users'); }); it('should call getDailySmokers with users passed to the saga', () => { const { value } = iterator.next(); expect(value).toEqual(call(getDailySmokers, 'users')); }); it('should call sortSmokersByState with users returned by getDailySmokers', () => { const { value } = iterator.next('dailySmokers'); expect(value).toEqual(call(sortSmokersByState, 'dailySmokers')); }); it('should call notifyQualified with qualified and asked key then notifyDubious with dubious key', () => { let { value } = iterator.next({ asked: ['asked'], qualified: ['qualified'], dubious: 'dubious' }); expect(value).toEqual(call(notifyDubious, 'dubious')); value = iterator.next().value; expect(value).toEqual(call(notifyQualified, ['asked', 'qualified'])); }); }); Découpage Passons maintenant à l’implémentation de notre bot. Il est composé de 3 lambdas : subscribe répond au post du formulaire ; il crée un utilisateur et envoie le premier SMS dailyMessage est exécuté par un cron qui envoie le message journalier à chaque utilisateur, le message étant basé sur l’état de l’utilisateur botConversation est appelé par Octopush et traite les réponses de l’utilisateur Passons rapidement sur la lambda subscribe, qui est activée par une route POST appelée par un simple formulaire statique hébergé sur s3. subscribe: handler: src/serverless/index.subscribe events: - http: method: POST integration: lambda path: subscribe cors: true Traitement des messages entrants La lambdabotConversationest appelé par Octopush via une route POST: dailyMessage: handler: src/serverless/index.dailyMessage events: - schedule: rate: cron(0 8 ? * * *) enabled: false La syntaxe cron pour AWS prend 6 paramètres: minutes, heures, jour du mois, mois, jour de la semaine, et année. On ne peut pas activer simultanément le jour du mois et le jour de la semaine ; pour ignorer l’un des deux on utilise le caractère ?. La lambda dailyMessage récupère tous les utilisateurs avec la commande scan de DynamoDB. scan accepte en paramètres batchSize et exclusiveStartKey, qui permettent de réaliser la commande en batch. batchSize spécifie le nombre de résultats à retourner, et exclusiveStartKey précise la clef à partir de laquelle reprendre la requête. Le résultat de scan inclut la dernière clef retournée. Pour exécuter les traitements en série, nous utilisons une récursion sur le générateur. function* dailyMessage() { /// ... first batch yield* dailyMessage(lastKey); } Ensuite, chaque utilisateur est trié suivant son état dubious/qualified, et le nombre de jours restants. Les utilisateurs dubious sont les utilisateurs qui se sont inscris, mais n’ont jamais ou mal répondu à la première question. dailyMessage va alors les relancer. Enfin, les utilisateur vont être triés selon le nombre de jours qu’il leur reste: S’ils sont à la fin du programme: si leur consommation est descendue à 0 cigarettes sur les 3 derniers jours, nous les félicitons. Sinon, nous les invitons à recommencer. S’ils sont à la fin d’une semaine: nous spécifions un nouvel objectif. Dans tout les autres cas: On décrémente le nombre de jour restant et on demande à l’utilisateur combien de cigarettes il a fumé hier. L’implémentation de cette machine a été assez simple se compose de quelques if impriqués – rien de très notable, à part l’apport bénéfique de sg qui simplifie les side effects. Traitement du langage naturel De plus en plus de librairies rendant le traitement en langage naturel (ou NLP pour Natural Language Processing) accessible apparaissent, et notemment en node.js: nlp_compromise natural Le NLP est un sujet coeur pour les bots quand il s’agit de traiter les questions. De notre côté, nous n’avions qu’à traiter des réponses, et dans un cadre très restreint. nlp nous a simplement permis de récupérer le nombre de cigarettes dans les messages envoyés par l’utilisateur. Que celui nous réponde at least 15 cigarettes, no more than fifteen cigarettes or 15, nlp nous retourne 15. Conclusion Le projet tobaccobot, a été l’occasion de nous familiariser avec plusieurs de technologies: serverless, aws lambda, aws dynamoDb, octopush. Serverless est un outil puissant, mais mettre en place le bon environnement de développement a demandé beaucoup d’expérimentation pour trouver la bonne configuration. De plus, nous avons passé beaucoup de temps à nous documenter et à configurer l’environnement serverless comparé à un serveur traditionnel. Cela dit, ce travail ayant été réalisé, la mise en place sera bien plus rapide à l’avenir. Une fois la partie serverless mise en place, le bot en lui même s’est révélé simple à implémenter, puisqu’il s’agit de prendre un événement (sms ou cron) et un état en entrée et de mettre à jour l’état et générer un message en sortie. La modélisation de la conversation est donc la partie la plus difficile. Il aurait été intéressant d’avoir à gérer une interaction avec un groupe d’utilisateur, ou une interaction plus variée. Tout bien considéré, cela reste une bonne introduction à la réalisation d’un bot. Le code de notre tobaccobot est disponible sur github: https://github.com/marmelab/tobaccobot Retrouvez l’article original en cliquant ici !

Create a cross platf...

First experience with react-native and react-native-web To create a native app with code sharing today there are 2 main approaches: Hybrid app: write in JavaScript, HTML and CSS, and the entire code is embedded and run in a web view in mobile. Like Phonegap. JavaScript engine + native UI: write in JavaScript. Ui components are translated into native UI components. Other codes are run in a JavaScript engine provided by the mobile system. React Native is a framework represented by the second philosophy. It lets you create a mobile app using JavaScript. As a web app developer with not so much mobile background, it could be a good way to start a mobile app. React Native is based on React, same design as React, so he should have a good integration with other react lib. It is based on version 0.56.RC now, not yet a major version. But looking at who’s actually using React Native: Facebook, YouTube, Skype etc, we could have confidence in it. React Native is a Facebook project. To make real code sharing, we expect to have at the same time a web app without re-writing the UI part. That comes with the “react-native-web” framework, who brings the Components and APIs of React Native to web. As mentioned in React Native Web home pages, it is used in production by Twitter Lite. It is also supported by react-scripts. So let we start an experience of a cross device application with this two framework. I want to do something further than a hello world example. But let me start with initializing the project. Initialize a project There are two ways to initiate a React Native project as explained here. Create React Native App A quick way to create and start a mobile app if you have a device on which you want to run (otherwise you will need to install an emulator). npm install -g create-react-native-app create-react-native-app AwesomeProject It will be hosted by “expo” configuration so you can quickly run your native app within Expo client app. Scripts run will deploy mobile app within an expo container. React-native-cli In this case you will need a full mobile development environment which means XCode for iOS and Android Studio and Android SDK for Android etc to start with. npm install -g react-native-cli react-native init MyNote The script creates two folders additional to “Android” and “iOS” and initiate a default setting for native app without “expo”. This should be the best way for initiating a standard project. To launch a simulator, take iOS for example, you can run react-native run-ios Or you can also open .xcodeproj in XCode and run the project. You can also do this job later in your react-native project with react-native upgrade Either the way of initialization, we now have a runnable native app project. So far so good. Everything goes well. Configure the native project as a web app React Native translates its UI components to native platform components for iOS and Android, and React-native-web will do the job for a web platform. Check its > Github page < We will need to add a few things to make web app available : On react-dom, react-native-web, babel-plugin-react-native-web> In the entrypoint index.web.js, instead of the classic react way to render you application with the DOM, we will do this in the React Native way, using AppRegistry. So my entry point is something like this. import App from './App'; import React from 'react'; import { AppRegistry } from 'react-native'; AppRegistry.registerComponent('MyNote', () => App); AppRegistry.runApplication('MyNote', { initialProps: {}, rootTag: document.getElementById('react-native-app') }); The thing is, react-script can launch a react-native project and automatically do the magical alias to react-native-web, but the embedded webpack config required a specific folder structure that does not so much fit the structure created by react-native. So I create my  webpack.config.js and run a webpack dev server. In the webpack config, we need a babel loader for everything expected to be compiled by Babel. And plug our “babel-plugin-react-native-web” here to take care of the aliases ‘react-native’ to ‘react-native-web’. Or you can also do this in you module export resolve. And don‘t forget to set your entry index.web.js. After all these, my project and my package.json look like this And I can now run my native app with xcode and on the other side my web app with script npm run web. When the code changes, a simple Cmd+R in simulator or in browser will reload app. A little bit settings for the web part, it’s a pity that the web app initialization is not included by react-native init step. And now we are ready for our develop environment. Developments : UI component and API The development is very similar to classic react. Just using React Native component instead of DOM component. The basic components of React Native are quite simple. View, Text, Image, TextInput, TouchableHighlight etc. You can easily associate a DOM interface using div, img, input, a with them. Most apps will end up using just these basic components. Component style is defined by ‘style’ prop. If you are familiar with CSS, the style name and value match usually how it works on web. The events and handlers are quite similar to DOM as well. For example a TextInput component has onChange, onKeyPress, onFocus and onBlur props. For a web developer, you should be able to make it out quite well for this part. More advanced native components are also available in react-native. Most common components are well supported in react-native-web. The latest version of react-native-web adds implement for SectionList. Still there are platform specific components. DatePicker is one of them. We can regret that iOS and Android could not reach an agreement with DatePicker interface. React native provides a Platform API to make platform specific codes. For a DatePicker for example, we could have something like this :   const DatePicker = Platform.select({ ios: <DatePickerIOS />, android: <DatePickerAndroid />, web: <input type='date' /> }) Many third party libraries exist today to unify the two mobiles platform codes (react-native-datepicker for example), but few of them includes web support. Responsive React-native component use FlexBox layout. FlexBox is a helpful tool to create responsive app. On the web side it will be translated into css flexbox properties, that means old browsers will not be supported, flexDirection, alignItems, justifyContent, alignSelf, flex properties are available in react-native, and work in the same way as in CSS. Dimension is another helpful API. Dimension.get can give the current height and width. You can create a dynamic rendering and styling logic depending on it. The calculation should be done at every render to guarantee the dimensions up to date with any changing due to device rotation or browser resize. Dimension API provides a change event listener. Platform API is also a choice to build rendering logic. In that case we usually would like to differentiate between a small mobile screen and a large browser window on laptop. Well actually Platform.OS and Platform.select has 3 possible values “iOS”, “Android” and “Web”. I don’t think it can distinguish an iPhone from a iPad, so your mobile screen layout may not be suitable for a tablet. Navigation Navigation is a hard part to make code sharing successful. Each platform has its own way to manage navigation and history. Unfortunately it is also one of the essential part of app. React-native does not provide a “official” API for navigation. It recommends some available navigation components. I’ve tried React Navigation which support both the mobile and web platform. Although, after trying several combination of react native and react navigation, I fixed in version 0.54.0 of react-native and 1.5.8 of react-navigation. Cause after react-navigation 2.0, web support is broken. And I had several problems to work react-navigation 1.5.8 with other versions of react-native. Live this instability in JS world. Well the fix for web is in V2 roadmap. React Navigation provides basic features of navigation like navigate, history, routing. Advanced features that could be interesting in React Navigation : Sub routing, multi-routing Deep link in 3 platform Customizable navigator Provides customizable UI for navigation like header and tab. Even though deep link is supported, I didn’t find any option to change URL when path changed in web platform. That needs to be implemented manually. Other classic features which do not need UI action works well in mobile device as in web browser, like async call, await, integration with Redux etc, as the code runs in a Javascript environment. If you use a JS library that does not reference DOM API, you should not have any surprise. Conclusion React native, with the help of React-native-web propose a quite simple way to create a cross device application. It include the essential requirement of an application with a possibility to customize. It comes with a rich ecosystem around React. It does not require a great mobile background to start and deploy a mobile app. It makes real code sharing between web and mobiles in 90% case. UI development is very similar to HTML development. Ecosystem of react-native is very dynamic. Compare with hybrid app, native component and API are used in react-native app (for mobile platforms). There are still some drawbacks To integrate web app with react-native, one will need some manual work. Even though, react-native + react-native-web is still a good choice to make a cross device app with a real code sharing and gains significantly in productivity.

Dissecting Webpack: ...

In the previous post, we had discussed the setting up of Webpack configuration file and kick-starting it with Webpack development server. But what makes Webpack a one-stop bundler are the loaders and plugins. Loaders help Webpack to transform code or help in the development aspect whereas plugins come in at the end when the bundling is happening to enhance or optimise the performance of the application. The following figure shows some recommended loaders and plugins. Credit: Roohiya Dudukela As some of you may have noticed in the configuration chart we have above, there are 2 attributes that have not been mentioned yet: module: {rules: []}, plugins: [] The loaders configuration goes into modules.rules array while plugins configuration goes into…plugins array. So that was easy. Let’s dive right into, firstly, loaders. Loaders Babel Loader npm install babel-loader --save-dev This loader transpiles >ES5 code to ES5. The configuration is as follows: This informs Webpack to use babel-loader only on jsx files and exclude looking into node modules. Along with the loader, there are a bunch of dependencies to be installed: npm install babel-core --save-dev One may ask why we need a babel-loader when we already have a babel-polyfill. To understand this, we need to see what is the function of each tool. This video has a good explanation on the difference. In short, babel-loader takes care of transforming syntax that is above ES5, and the babel-polyfill is called to create new functions and methods, that browsers don’t support, on the fly. Both complement each other and are needed to handle different parts of the modern JS. For example, ‘const’ is transpiled to ‘var’ & and arrow function will be transpiled to an anonymous function. const myArray = new BetterArray( [1, 2, 3] ) → var myArray = new BetterArray( [1, 2, 3] ) var nums = list.map( ( v, i ) => v + i ) → var nums = list.map( function(v, i) { return v + i } ); CSS Loader & Style Loader Since Webpack only understands Javascript, we need to add loaders to tell it how to handle CSS files. npm install css-loader --save-dev CSS loader will look into all the css imports and urls and return a string (as shown below) that will be part of the main js. ... \"body {\\n background-color: pink;\\n\"... Since it is the part of the JS file, the browser does not have the capability to recognise and extract the css code from the js, so the styles will not be applied. npm install style-loader --save-dev What we need is a style loader that extracts the css string out of the js bundle and inject it into styles tag of the html file. With the above configuration, which specifies to use style and css loaders (in that order shown above) on all files, excluding ones in node modules, with a .css extension, we have the styles applied and CSS taken care of. File Loader & URL Loader Another set of assets that we need to explicitly tell Webpack how to handle is images and fonts. How we usually manage images is either to inject them inline in the <img> tags or store them in a server to make network requests to render the images. npm install file-loader --save-dev File loader only alters the path name of the file to give it a public URL. It looks for all the imports and urls of images being used and formats the path name accordingly. With the above configuration, the file-loader will load all the assets with the specified extensions, and place the images in the ‘/images’ folder with the format [name]_[hash].[ext]. Below are a couple of examples: background-image: url('images/dog.png') → images/dog_436bd585...png import Cat from 'images/cat.jpg' → images/cat_875ds32132dsda3...jpg However, if we place all the images on a server, the overheads for making multiple network request could dampen the performance. There is an advanced loader that is a wrapper around the file-loader, which is known as url-loader. npm install url-loader --save-dev What this loader does is handle images based on their size. This configuration looks a lot like the one for file-loader, but the interesting part is the attribute ‘limit’. This states the size limit of the image. < 8kb ? <img src='background-image: url(data:image/png;base64,iGwfd..)' /> : images/dog_876bd585bdc8a5cc40633ffefdb7a4a5.png If, in this case an image is less than 8kb, the url-loader will convert the image to base 64 string and inject it to the <img> tag, else it will default to file-loader which will create a public URL to be saved in the server. Images up to a certain size will be converted to base64 without slowing down the application. The limit can be toggled to gauge the most optimised performance for the project. Standard Loader This is an optional loader to help in development. This loader lints code based on Standard JS rules. This loads as a pre-loader and lints all .jsx files, excluding ones in node modules. In 4.0 For using loaders in Webpack 4.0 you still have to create a configuration file, and most of the configurations remains. Plugins HTML Webpack Plugin This plugin helps in the creation of a html file from scratch and injects variables into the HTML file. npm install html-webpack-plugin --save-dev Following is an example of what variables to set. These variables can be used in the index.html file such as: <title><%= htmlWebpackPlugin.options.title %></title> <link rel="icon" href="<%= htmlWebpackPlugin.options.favicon%>"> <div id="<%= htmlWebpackPlugin.options.appMountId%>"></div> Extract Text Webpack Plugin This plugin allows to extract text of any kind to a separate file. This configuration, uses CSS loader to resolve all the CSS but instead of injecting them into the style tags, with the help of style-loader, the plugin will take the CSS string and push it into another file. The extracted file will be rendered in parallel to the bundled file. Doing this has its pros and cons. Let’s go through them. Pros are that having the CSS in a separate file and not in style tags, will obviously reduce the style tags used and consequently the bundle size. With smaller bundle size, the load time will be faster. As mentioned earlier the CSS file renders in parallel which eliminates the flash of unstyled content. However, enabling this extraction will stop hot reload from working. And since we have another file, depending on the size, the compilation time will be longer. It is recommended to have this plugin only for production mode. In 4.0 Extract-text-webpack-plugin is deprecated and is replaced by mini-css-extract-plugin. Common Chunks Plugin Another very important aspect of bundling the project is code splitting. This can substantially optimise the performance of the application. This plugin helps to split out common codes in the project into chunks. These chunks can be loaded on demand or in parallel. This aims to achieve smaller bundle size and loading prioritisation. Credit: Roohiya Dudukela A small js file can grow to be really big which does not bode well for any application. So, it can be chunked into, for example, Multiple entries Chunks with common codes within multiple entries (Lodash) Vendor libraries that do not change as frequently as the main codebase (React) In the above example, we are adding another entry to the Webpack bundle, ‘vendor’. This is chunking out 3rd party libraries. With this separate chunk, our main chunk will be reduced considerably! In 4.0 CommonsChunkPlugin has been deprecated and instead the APIs optimize.splitChunks & optimization.runtimeChunk can be used. This is possible with the new plugin, SplitChunksPlugin. Instead of manually specifying what to chunk, the plugin is smart enough to identify modules that need to be chunked. UglifyJS Webpack Plugin This is a familiar plugin which obfuscates code and handles dead code elimination. Mode Webpack.common.js A common Webpack simply consists of configurations that are common between dev and production: Babel-polyfill Babel-loader CSS & Style loader URL loader So the configuration for Webpack.common.js is: Webpack.dev.js Development Server with hot reload Standard loader CSS loader with sourcemap (for debugging purposes) The configuration for Webpack is: Webpack.prod.js CSS extraction Uglify & dead code elimination Code splitting And the configuration for Webpack.prod.js is: Webpack Merge We can combine the common and dev, also the common and prod with Webpack-merge. In 4.0 Without the need of Webpack merge, we can use the script configuration to specify the -- mode flag and Webpack will take care of the rest. "dev": "webpack — mode development", "build": "webpack — mode production" For a complete react-redux webpack configuration, please take a look at this Github Repo for guidance. So hopefully this has been an enlightening journey, making you feel more under control over Webpack configurations. No more running away from this! References 4 Key Concepts of Webpack | Netlify Webpack is JavaScript module bundler that has taken the world by storm, but a lack of great docs and wealth of…www.netlify.com Extract Text Plugin In the last lesson, we got our styles working all good; getting the css and scss files bundled and then getting the…medium.com Plugins Installation Getting Started Asset Management Output Management Development Hot Module Replacement Tree Shaking…webpack.js.org webpack-contrib/file-loader file-loader — A file loader for webpackgithub.com DevServer Installation Getting Started Asset Management Output Management Development Hot Module Replacement Tree Shaking…webpack.js.org Webpack 4 Tutorial: from 0 Conf to Production Mode (Updated) webpack 4 is out! The popular module bundler gets a massive update. webpack 4, what’s new? A massive performance…www.valentinog.com

Dissecting Webpack: ...

Many front-end developers shy away from Webpack, much less tackle it head-on. The nature of Webpack is as such, that it has too many configuration options and tinkering with it could break the application. So this article attempts to simplify major concepts in the building of a frontend project specifically with React. Sprinkled in the article are little tips to prepare for Webpack 4.0! What is Webpack? Overview A frontend application has multiple files, .js, .jsx, .png, .jpg, .css, .ttf. We cannot possibly take these bunch of files and dump it in the production web server. The performance load time and overheads will be ghastly. What we need is a single bundled file of js that holds the structure and logic of the components and application, a single file of CSS for styles, and HTML file to render the DOM and an assets folders for images and fonts. There have been many tools and task runners that have helped in the bundling process. But Webpack has emerged to be the one-stop solution for many React frontend developers.                                                                                                                                    Credit: Roohiya Dudukela Setup To get started off, we have to install webpack and webpack-cli. npm install webpack -g npm install webpack-cli -g We also need a basic project to bundle up. For that we can create a new folder called, ‘react-webpack’. mkdir react-webpack We need a package.json file in our project, that would later be required to define our start scripts. Change directory into the ‘my-app’ folder and run npm init. cd react-webpack npm init Create a index.js file that will serve as the entry point to the Webpack. touch index.js In the index.js, we can add a simple console.log. console.log('Hello World') We can now use the webpack-cli to bundle this index.js into dist/bundle.js. webpack index.js dist/bundle.js And the minified code, in bundle.js, would look something like this: ...(function(module, exports) {\n\neval(\"console.log('Hello Worl... *Update In the latest webpack-cli version, Simply calling ‘webpack index.js’ will auto-generate a distribution folder and bundled file called ‘main.js’   The project structure should look like this at this point: But as the code base grows, it is impossible to keep using the ‘webpack-cli’ to bundle the code, for every change we make. So what Webpack allows us to do, is to feed it a configuration object that consists of 5 important features. Entry / Output Dev Server Loaders Plugins Mode Entry / Output First up, we need to create a file for the configuration. touch webpack.config.js In that file, ‘webpack’ library is required, along with declaration of the config object and finally to export this config object that will be fed to Webpack. Next, you guessed it right, we have to populate the configuration object. Context As a good practice, all the source files should be in a separate folder and not in the root folder. Specifying context tells Webpack to look for all source files, starting from ‘index.js’, hereon. This eliminates the need to add dots and slashes, using the relative path, to get to a specific file. To set this up, we require the ‘path’ library to help map out the absolute paths. npm install --save-dev path Entry Since we have set up the context or the base directory, the entry file can be stated as just ‘index.js’ instead of ‘./index.js’. This is where Webpack starts to chart its dependency graph. Output The output file is what the Webpack churns out at the end that contains the compact bundle of the whole application with the dependencies intact. Resolve This attribute is to tell Webpack which files and folders to look into when building its dependency graph. Babel Before we continue to the next step, which involves writing fancy javascript codes, some Babel configuration is needed. Not all browsers are able to handle ES6 syntax. Therefore, we need to bridge the gap by adding a ‘babel-polyfill’ in the entry attribute. For the setup, babeljs.io has pretty neat steps and explanations. Babel-polyfill creates new functions on the fly for browsers that do not have the support for JS code that is above ES5. You can take a look at the browser compatibility table to check if a babel-polyfill is required for the project. This is not the complete configuration for the Babel, we will be adding babel-loader, later on, to provide a complete support for ES6 and above. The following, along with babel-polyfill, need to be installed for React projects. npm install --save babel-polyfill npm install --save-dev babel-preset-env npm install --save-dev babel-preset-react npm install --save-dev babel-preset-stage-0 For a setup of Babel, add a .babelrc in the folder. And add this piece of code: What this specifies is the Babel presets. A preset is a set of plugins that supports particular language features. For example, ‘react’ preset adds support for JSX and ‘es2015’ for ES6 features. To use Javascript features beyond ES7, these features can exist in various ‘stages’. The stages are defined as TC39 categories. So, to enable these features we use ‘stage-0’ as an idea phase or proposal but this helps us to write neat code. { "presets": [ "es2015", "react", "stage-0" ] } The setup up till this moment, should look something like this: In 4.0 No entry and output point need to defined explicitly: it will take the index.js in the src folder as the default. (Though you can still override the default by stating the path in the script in package.json.) Dev Server Setup This goes without saying, we need a development server to launch our application on the browser. Webpack provides us with its own dev server, webpack-dev-server. Installing webpack and webpack-cli to be saved within this project. npm install webpack-dev-server --save-dev npm install webpack --save-dev npm install webpack-cli --save-dev All that needs to be done, is to add a devServer attribute to the above configuration. With this, we will have a simple dev server running that is serving files from the current directory. Run server Now to be able to run the server, we need to tap in the package.json of the application. In the “scripts” attribute of the object, we add a command to run webpack-dev-server with hot attribute enabled. The hot attribute, allows the server to watch for changes in code. If there are any changes done, it will refresh the page automatically. 'scripts': { 'start': 'webpack-dev-server --hot' } To run this, we need to call the command: npm start And voila! We can see the application running on localhost:3000. In 4.0 This is as per Webpack 4.0 specifications as well. For the initial setup, you may look at this Github Repo for guidance. Up till this point, we have only scraped the surface of the Webpack. There’s still much to be explored. We are just getting to the exciting parts. The magic of Webpack lies in the ability to use loaders and plugins, which will be covered in Part II of this series. References 4 Key Concepts of Webpack | Netlify Webpack is JavaScript module bundler that has taken the world by storm, but a lack of great docs and wealth of…www.netlify.com Extract Text Plugin In the last lesson, we got our styles working all good; getting the css and scss files bundled and then getting the…medium.com Plugins Installation Getting Started Asset Management Output Management Development Hot Module Replacement Tree Shaking…webpack.js.org webpack-contrib/file-loader file-loader – A file loader for webpackgithub.com DevServer Installation Getting Started Asset Management Output Management Development Hot Module Replacement Tree Shaking…webpack.js.org Webpack 4 Tutorial: from 0 Conf to Production Mode (Updated) webpack 4 is out! The popular module bundler gets a massive update. webpack 4, what’s new? A massive performance…www.valentinog.com