Aller au contenu

Utilisation des coroutines dans les stories

Fonctionnalité expérimentale

L'API Coroutines de Tock est expérimentale. Elle peut contenir des bugs, ou changer (breaking changes) sans préavis.

Prérequis de lecture : Bot intégré

Mise en place

L'API coroutine fait partie du module tock-bot-engine : la dépendance bot-toolkit du mode intégré suffit. Une story coroutine est une StoryDefinition comme les autres : déclarez-la dans la liste des stories du bot, où elle peut être mélangée avec des stories classiques :

val openBot = bot(
    "bot_open_data",
    stories = listOf(greetings, departures, arrivals, search), // search est une story coroutine
    hello = greetings,
)

Classes

Les classes relevant de l'API Coroutines de Tock sont préfixées par Async. On retrouve une hiérarchie de classes très similaire à celles de l'API Bot intégré :

classDiagram
    class StoryDefinition {
        <<interface>>
        +StoryHandler storyHandler
        +Set[StoryStep] steps
    }
    class AsyncStoryDefinition {
        <<interface>>
        +AsyncStoryHandler storyHandler
        +Set[AsyncStoryStep] steps
    }
    class StoryHandler {
        <<interface>>
    }
    class AsyncStoryHandler {
        <<interface>>
    }
    class AsyncStoryHandlerBase {
        <<abstract>>
    }
    class AsyncDelegatingStoryHandlerBase {
        <<abstract>>
    }
    class AsyncConfigurableStoryHandler
    class StoryStepDef {
        <<interface>>
    }
    class StoryStep {
        <<interface>>
    }
    class AsyncStoryStep {
        <<interface>>
    }
    class AsyncStoryHandling {
        <<interface>>
    }
    class AsyncStoryHandlingBase {
        +AsyncConnectorHandling connector
    }
    class AsyncConnectorHandling {
        <<interface>>
    }
    class AsyncConnectorHandlingBase
    class AsyncBus

    StoryDefinition <|.. AsyncStoryDefinition
    StoryDefinition o-- StoryHandler
    StoryDefinition o-- StoryStepDef
    AsyncStoryDefinition o-- AsyncStoryHandler
    AsyncStoryDefinition o-- AsyncStoryStep
    AsyncStoryHandler <|.. StoryHandler
    AsyncStoryHandler --> AsyncStoryHandling
    AsyncStoryHandlerBase ..|> AsyncStoryHandler
    AsyncDelegatingStoryHandlerBase ..|> AsyncStoryHandlerBase
    AsyncConfigurableStoryHandler --|> AsyncDelegatingStoryHandlerBase
    StoryStep <|.. StoryStepDef
    AsyncStoryStep <|.. StoryStepDef
    AsyncStoryHandlingBase ..|> AsyncStoryHandling
    AsyncStoryHandlingBase ..|> AsyncBus
    AsyncStoryHandlingBase o-- AsyncConnectorHandling
    AsyncConnectorHandlingBase ..|> AsyncConnectorHandling
    AsyncConnectorHandlingBase ..|> AsyncBus

Les classes qui nous intéressent particulièrement sont les suivantes :

  • AsyncStoryDefinition : StoryDefinition spécialisée pour AsyncStoryHandler et AsyncStoryStep
  • AsyncBus : équivalent de Bus
  • AsyncStoryHandlingBase : équivalent de StoryHandlerDefinitionBase
  • AsyncConnectorHandlingBase : équivalent de ConnectorStoryHandlerBase
  • AsyncStoryStep : équivalent de StoryStep

Voici l'exemple donné pour le bot intégré, adapté pour cette nouvelle API, et avec une nouvelle fonctionnalité d'affichage des trajets retour (pour démontrer les appels en parallèle) :

// The data required by the story to execute
private data class SearchArgs(
    val destination: Place,
    val origin: Place,
    val date: LocalDateTime,
    val returnDate: LocalDateTime?,
)

/**
 * This story takes an origin, a destination, a date and an optional return date and displays the first available itinerary
 */
// the story definition, specifying the primary and secondary intents, a handling class, and a precondition checker
val search = storyDef(
    "search", // Main starter intent and story identifier
    setOf(indicate_origin), // Other starter intent (starts the story if detected)
    setOf(indicate_location), // Secondary intent (keeps the story running if detected while in the story)
    handling = ::SearchHandling, // Constructor reference for your AsyncStoryHandling implementation
) { /* Precondition checker */
    // This code assumes that location, destination, origin, departureDate and returnDate are extension properties
    // on the AsyncBus

    // if the current intent is indicate_location and the location entity has a value, store the value
    // in the appropriate variable
    if (matchesIntent(indicate_location) && location != null) {
        if (destination == null || origin != null) {
            destination = returnsAndRemoveLocation()
        } else {
            origin = returnsAndRemoveLocation()
        }
    }    

    //check mandatory entities
    when {
        destination == null -> end("Pour quelle destination?")
        origin == null -> end("Pour quelle origine?")
        departureDate == null -> end("Quand souhaitez-vous partir?")
    }

    // args passed to the main handling class
    SearchArgs(destination, origin, departureDate, returnDate)
}

// Specify dedicated handling for two connectors via the following annotations.
// Your story can support as many or as few connectors as you want, among all those supported by the Tock framework.
@WhatsAppCloudHandler(WhatsAppCloudSearchConnector::class) // Connector-specific handling for WhatsApp
@MessengerHandler(MessengerSearchConnector::class) // Connector-specific handling for Facebook Messenger
class SearchHandling(bus: AsyncBus, private val args: SearchArgs) : AsyncStoryHandlingBase<SearchConnector>(bus) {

    override suspend fun answer() {
        // extract the args passed by the precondition checker
        val (d, o, date, returnDate) = args

        // First text message, with string interpolation
        send("De {0} à {1}", o, d)
        // Second text message, with string interpolation using a custom DateTimeFormatter
        send("Départ le {0}", date by datetimeFormat)
        // If the current connector supports it, the two messages above will be sent immediately, without waiting
        // for the API call below to complete.
        // This serves as a confirmation that the bot is processing the request before it can show the result.

        // Query in parallel our API for the journeys and the return journeys
        val (journeys, journeysBack) = coroutineScope {
            // First async call
            val journeys = async { SncfOpenDataClient.journey(o, d, date) }
            // Second async call, optional
            val journeysBack = returnDate?.let { async { SncfOpenDataClient.journey(d, o, it) } }
            // Wait for both calls to be finished
            Pair(journeys.await(), journeysBack?.await())
        }

        if (journeys.isEmpty()) {
            end("Désolé, aucun itinéraire trouvé :(")
        } else {
            send("Voici la première proposition :")
            // The calls to `c` below will fail with an NPE if you didn't specify an implementation for the current connector
            // To be null-safe, you can use the `connector` field instead
            if (journeysBack.isNullOrEmpty()) {
                // Not a round trip -> end with the one-way journey
                c.endWithJourney(journeys.first())
            } else {
                // Round-trip -> send both
                c.sendJourney(journeys.first())
                send("Voici le voyage retour :")
                c.endWithJourney(journeysBack.first())
            }
        }
    }
}

/** Handles connector-specific code (rich message creation) */
sealed class SearchConnector(context: SearchHandling) : AsyncConnectorHandlingBase<SearchHandling>(context) {

    // Utility method used in subclasses
    protected fun Section.title(): CharSequence = i18n("{0} - {1}", from, to)

    // Method used by the story
    fun sendJourney(journey: Journey) = send {
        journey(journey.publicTransportSections())
    }

    // Method used by the story
    fun endWithJourney(journey: Journey) = end {
        journey(journey.publicTransportSections())
    }

    // Implementation that generates a rich display message for a given list of sections
    protected abstract fun Bus<*>.journey(sections: List<Section>): ConnectorMessage

}

/** Implementation of our connector for the Messenger platform. The WhatsApp variant is left as an exercise to the reader. */
class MessengerSearchConnector(context: SearchHandling) : SearchConnector(context) {

    override fun Bus<*>.journey(sections: List<Section>): ConnectorMessage =
        // Display our journey as a carousel
        genericTemplate(
            sections.map { section ->
                with(section) {
                    genericElement(
                        title(),
                        content(),
                        trainImage
                    )
                }
            }
        )
}
Chat with Tock
×