All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fr.vsct.tock.bot.engine.BotRepository.kt Maven / Gradle / Ivy

/*
 * Copyright (C) 2017 VSCT
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package fr.vsct.tock.bot.engine

import fr.vsct.tock.bot.admin.bot.BotApplicationConfiguration
import fr.vsct.tock.bot.admin.bot.BotApplicationConfigurationDAO
import fr.vsct.tock.bot.connector.Connector
import fr.vsct.tock.bot.connector.ConnectorBase
import fr.vsct.tock.bot.connector.ConnectorCallback
import fr.vsct.tock.bot.connector.ConnectorConfiguration
import fr.vsct.tock.bot.connector.ConnectorProvider
import fr.vsct.tock.bot.connector.ConnectorType
import fr.vsct.tock.bot.definition.BotDefinition
import fr.vsct.tock.bot.definition.BotProvider
import fr.vsct.tock.bot.definition.StoryHandlerListener
import fr.vsct.tock.bot.engine.config.BotConfigurationSynchronizer
import fr.vsct.tock.bot.engine.event.Event
import fr.vsct.tock.bot.engine.monitoring.RequestTimer
import fr.vsct.tock.bot.engine.nlp.BuiltInKeywordListener
import fr.vsct.tock.bot.engine.nlp.NlpListener
import fr.vsct.tock.nlp.api.client.NlpClient
import fr.vsct.tock.shared.DEFAULT_APP_NAMESPACE
import fr.vsct.tock.shared.Executor
import fr.vsct.tock.shared.defaultLocale
import fr.vsct.tock.shared.error
import fr.vsct.tock.shared.injector
import fr.vsct.tock.shared.provide
import fr.vsct.tock.shared.tockAppDefaultNamespace
import fr.vsct.tock.shared.vertx.vertx
import io.vertx.ext.web.Router
import io.vertx.ext.web.RoutingContext
import mu.KotlinLogging

/**
 * Advanced bot configuration.
 *
 * [fr.vsct.tock.bot.registerAndInstallBot] method is the preferred way to start a bot in most use cases.
 */
object BotRepository {

    private val logger = KotlinLogging.logger {}

    private val botConfigurationDAO: BotApplicationConfigurationDAO get() = injector.provide()
    internal val botProviders: MutableSet = mutableSetOf()
    internal val storyHandlerListeners: MutableList = mutableListOf()
    internal val nlpListeners: MutableList = mutableListOf(BuiltInKeywordListener)
    private val nlpClient: NlpClient get() = injector.provide()
    private val executor: Executor get() = injector.provide()

    internal val connectorProviders: MutableSet = mutableSetOf(
        object : ConnectorProvider {
            override val connectorType: ConnectorType = ConnectorType.none
            override fun connector(connectorConfiguration: ConnectorConfiguration): Connector =
                object : ConnectorBase(ConnectorType.none) {
                    override fun register(controller: ConnectorController) = Unit

                    override fun send(event: Event, callback: ConnectorCallback, delayInMs: Long) = Unit
                }
        }
    )


    /**
     * Request timer for connectors.
     */
    @Volatile
    var requestTimer: RequestTimer = object : RequestTimer {}

    /**
     * healthcheck handler to answer to GET /healthcheck.
     */
    @Volatile
    var healthcheckHandler: (RoutingContext) -> Unit = {
        executor.executeBlocking {
            it.response().setStatusCode(if (nlpClient.healthcheck()) 200 else 500).end()
        }
    }

    /**
     * Registers a new [ConnectorProvider].
     */
    fun registerConnectorProvider(connectorProvider: ConnectorProvider) {
        connectorProviders.add(connectorProvider)
    }

    /**
     * Registers a new [BotProvider].
     */
    fun registerBotProvider(bot: BotProvider) {
        botProviders.add(bot)
    }

    /**
     * Registers a new [StoryHandlerListener].
     */
    fun registerStoryHandlerListener(listener: StoryHandlerListener) {
        storyHandlerListeners.add(listener)
    }

    /**
     * Registers an new [NlpListener].
     */
    fun registerNlpListener(listener: NlpListener) {
        nlpListeners.add(listener)
    }

    /**
     * Installs the bot(s).
     *
     * @param routerHandlers the additional router handlers
     * @param adminRestConnectorInstaller the (optional) linked [fr.vsct.tock.bot.connector.rest.RestConnector] installer.
     */
    fun installBots(
        routerHandlers: List<(Router) -> Unit>,
        adminRestConnectorInstaller: (BotApplicationConfiguration) -> ConnectorConfiguration? = { null }
    ) {
        val verticle = BotVerticle()

        val connectorConfigurations = ConnectorConfigurationRepository.getConfigurations()

        //check connector id integrity
        connectorConfigurations
            .groupBy { it.connectorId }
            .values
            .firstOrNull { it.size != 1 }
            ?.apply {
                error("A least two configurations have the same connectorId: ${this}")
            }

        val bots = botProviders.map { it.botDefinition() }

        //install each bot
        bots.forEach {
            installBot(verticle, it, connectorConfigurations, adminRestConnectorInstaller)
        }

        //check that nlp applications exist
        bots.distinctBy { it.namespace to it.nlpModelName }
            .forEach { botDefinition ->
                try {
                    nlpClient.createApplication(
                        botDefinition.namespace,
                        botDefinition.nlpModelName,
                        defaultLocale
                    )?.apply {
                        logger.info { "nlp application initialized $namespace $name with locale $supportedLocales" }
                    }
                } catch (e: Exception) {
                    logger.error(e)
                }
            }

        //register services
        routerHandlers.forEachIndexed { index, handler ->
            verticle.registerServices("_handler_$index", handler)
        }

        //deploy verticle
        vertx.deployVerticle(verticle)
    }

    private fun installBot(
        verticle: BotVerticle,
        botDefinition: BotDefinition,
        connectorConfigurations: List,
        adminRestConnectorInstaller: (BotApplicationConfiguration) -> ConnectorConfiguration?
    ) {

        fun findConnectorProvider(connectorType: ConnectorType): ConnectorProvider {
            return connectorProviders.first { it.connectorType == connectorType }
        }

        val bot = Bot(botDefinition)
        val existingBotConfigurations = botConfigurationDAO.getConfigurationsByBotId(botDefinition.botId)
        val allConnectorConfigurations =
            connectorConfigurations +
                    existingBotConfigurations
                        .filter {
                            it.connectorType != ConnectorType.rest
                                    && connectorConfigurations.none { c -> it.applicationId == c.connectorId }
                        }
                        .map {
                            ConnectorConfiguration(it)
                        }
        val existingBotConfigurationsMap =
            existingBotConfigurations
                .groupBy { it.applicationId }
                .map { (key, value) ->
                    if (value.size > 1) {
                        logger.warn { "more than one configuration in database: $value" }
                    }
                    key to value.first()
                }
                .toMap()

        allConnectorConfigurations.forEach { baseConf ->
            findConnectorProvider(baseConf.type)
                .apply {
                    try {
                        //1 refresh connector conf
                        val conf = refreshBotConfiguration(baseConf, existingBotConfigurationsMap)
                        //2 create and install connector
                        val connector = connector(conf)
                        //3 set default namespace to bot namespace if not already set
                        if (tockAppDefaultNamespace == DEFAULT_APP_NAMESPACE) {
                            tockAppDefaultNamespace = bot.botDefinition.namespace
                        }
                        //4 update bot conf
                        val appConf = saveConfiguration(verticle, connector, conf, bot)

                        //5 monitor conf
                        BotConfigurationSynchronizer.monitor(bot)

                        //6 generate and install rest connector
                        adminRestConnectorInstaller.invoke(appConf)
                            ?.also {
                                val restConf = refreshBotConfiguration(it, existingBotConfigurationsMap)
                                saveConfiguration(
                                    verticle,
                                    findConnectorProvider(restConf.type).connector(restConf),
                                    restConf,
                                    bot
                                )
                            }
                    }catch(e:Exception) {
                        logger.error(e) {
                            "unable to install connector $baseConf"
                        }
                    }
                }
        }
    }

    private fun refreshBotConfiguration(
        configuration: ConnectorConfiguration,
        existingBotConfigurations: Map
    ): ConnectorConfiguration =
        existingBotConfigurations[configuration.connectorId]?.run {
            ConnectorConfiguration(configuration, this)
        } ?: configuration

    private fun saveConfiguration(
        verticle: BotVerticle,
        connector: Connector,
        configuration: ConnectorConfiguration,
        bot: Bot
    ): BotApplicationConfiguration {

        return with(bot.botDefinition) {
            val conf = BotApplicationConfiguration(
                configuration.connectorId.run { if (isBlank()) botId else this },
                botId,
                namespace,
                nlpModelName,
                configuration.type,
                configuration.ownerConnectorType,
                configuration.getName().run { if (isBlank()) botId else this },
                configuration.getBaseUrl(),
                configuration.parametersWithoutDefaultKeys(),
                path = configuration.path
            )

            TockConnectorController.register(connector, bot, verticle)

            botConfigurationDAO.updateIfNotManuallyModified(conf)
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy