
engine.Bot.kt Maven / Gradle / Ivy
/*
* Copyright (C) 2017/2021 e-voyageurs technologies
*
* 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 ai.tock.bot.engine
import ai.tock.bot.admin.bot.BotApplicationConfiguration
import ai.tock.bot.connector.ConnectorData
import ai.tock.bot.definition.BotDefinition
import ai.tock.bot.definition.Intent
import ai.tock.bot.definition.StoryDefinition
import ai.tock.bot.definition.StoryTag
import ai.tock.bot.engine.action.Action
import ai.tock.bot.engine.action.SendAttachment
import ai.tock.bot.engine.action.SendChoice
import ai.tock.bot.engine.action.SendLocation
import ai.tock.bot.engine.action.SendSentence
import ai.tock.bot.engine.config.BotDefinitionWrapper
import ai.tock.bot.engine.dialog.Dialog
import ai.tock.bot.engine.dialog.Story
import ai.tock.bot.engine.feature.DefaultFeatureType
import ai.tock.bot.engine.nlp.NlpController
import ai.tock.bot.engine.user.UserTimeline
import ai.tock.shared.injector
import com.github.salomonbrys.kodein.instance
import java.util.Locale
import mu.KotlinLogging
import kotlin.time.Duration
/**
*
*/
internal class Bot(botDefinitionBase: BotDefinition, val configuration: BotApplicationConfiguration, val supportedLocales: Set = emptySet()) {
companion object {
private val currentBus = ThreadLocal()
/**
* Helper method to returns the current bus,
* linked to the thread currently used by the handler.
* (warning: advanced usage only).
*/
internal fun retrieveCurrentBus(): BotBus? = currentBus.get()
}
private val logger = KotlinLogging.logger {}
private val nlp: NlpController by injector.instance()
val botDefinition: BotDefinitionWrapper = BotDefinitionWrapper(botDefinitionBase)
fun support(action: Action, userTimeline: UserTimeline, connector: ConnectorController, connectorData: ConnectorData): Double {
connector as TockConnectorController
loadProfileIfNotSet(connectorData, action, userTimeline, connector)
val dialog = getDialog(action, userTimeline)
parseAction(action, userTimeline, dialog, connector)
val story = getStory(userTimeline, dialog, action)
val bus = TockBotBus(connector, userTimeline, dialog, action, connectorData, botDefinition)
return story.support(bus)
}
/**
* Handle the user action.
*/
fun handle(action: Action, userTimeline: UserTimeline, connector: ConnectorController, connectorData: ConnectorData) {
connector as TockConnectorController
loadProfileIfNotSet(connectorData, action, userTimeline, connector)
val dialog = getDialog(action, userTimeline)
parseAction(action, userTimeline, dialog, connector)
var shouldRespondBeforeDisabling = false
if (userTimeline.userState.botDisabled && botDefinition.enableBot(userTimeline, dialog, action)) {
logger.debug { "Enable bot for $action" }
userTimeline.userState.botDisabled = false
botDefinition.botEnabledListener(action)
} else if (!userTimeline.userState.botDisabled && botDefinition.disableBot(userTimeline, dialog, action)) {
logger.debug { "Disable bot for $action" }
// in the case of stories with disabled tag we want to respond before disabling the bot
shouldRespondBeforeDisabling = botDefinition.hasDisableTagIntent(dialog)
if (!shouldRespondBeforeDisabling) {
userTimeline.userState.botDisabled = true
}
} // if user state has changed, always persist the user. If not, test if the state is persisted
else if (!botDefinition.hasToPersistAction(userTimeline, action)) {
connectorData.saveTimeline = false
}
if (!userTimeline.userState.botDisabled) {
dialog.state.currentIntent?.let { intent ->
connector.sendIntent(intent, action.applicationId, connectorData)
}
connector.startTypingInAnswerTo(action, connectorData)
val story = getStory(userTimeline, dialog, action)
val bus = TockBotBus(connector, userTimeline, dialog, action, connectorData, botDefinition)
if (bus.isFeatureEnabled(DefaultFeatureType.DISABLE_BOT)) {
logger.info { "bot is disabled for the application" }
bus.end("Bot is disabled")
return
}
try {
currentBus.set(bus)
story.handle(bus)
if (shouldRespondBeforeDisabling) {
userTimeline.userState.botDisabled = true
}
} finally {
currentBus.remove()
}
} else {
// refresh intent flag
userTimeline.userState.botDisabled = true
logger.debug { "bot is disabled for the user" }
}
}
private fun getDialog(action: Action, userTimeline: UserTimeline): Dialog {
return userTimeline.currentDialog ?: createDialog(action, userTimeline)
}
private fun createDialog(action: Action, userTimeline: UserTimeline): Dialog {
val newDialog = Dialog(setOf(userTimeline.playerId, action.recipientId))
userTimeline.dialogs.add(newDialog)
return newDialog
}
private fun getStory(userTimeline: UserTimeline, dialog: Dialog, action: Action): Story {
val newIntent = dialog.state.currentIntent
val previousStory = dialog.currentStory
val story = if (previousStory == null || (newIntent != null && (!previousStory.supportAction(userTimeline, dialog, action, newIntent)))) {
val storyDefinition: StoryDefinition = botDefinition.findStoryDefinition(newIntent?.name, action.applicationId)
val newStory = Story(storyDefinition, if (newIntent != null && storyDefinition.isStarterIntent(newIntent)) newIntent
else storyDefinition.mainIntent())
if (previousStory?.definition?.hasTag(StoryTag.ASK_AGAIN) == true && dialog.state.askAgainRound > 0 && !previousStory.supportIntent(newStory.definition.wrappedIntent())) {
dialog.state.askAgainRound--
dialog.state.hasCurrentAskAgainProcess = true
dialog.stories.add(previousStory)
previousStory
} else {
dialog.stories.add(newStory)
newStory
}
} else {
previousStory
}
story.computeCurrentStep(userTimeline, dialog, action, newIntent)
story.actions.add(action)
// update action state
action.state.intent = dialog.state.currentIntent?.name
action.state.step = story.step
return story
}
private fun parseAction(action: Action, userTimeline: UserTimeline, dialog: Dialog, connector: TockConnectorController) {
try {
when (action) {
is SendChoice -> {
parseChoice(action, dialog)
}
is SendLocation -> {
parseLocation(action, dialog)
}
is SendAttachment -> {
parseAttachment(action, dialog)
}
is SendSentence -> {
if (!action.hasEmptyText()) {
nlp.parseSentence(action, userTimeline, dialog, connector, botDefinition)
}
}
else -> logger.warn { "${action::class.simpleName} not yet supported" }
}
} finally {
// reinitialize lastActionState
dialog.state.nextActionState = null
}
}
private fun parseAttachment(attachment: SendAttachment, dialog: Dialog) {
botDefinition.handleAttachmentStory?.let { definition ->
definition.mainIntent().let {
dialog.state.currentIntent = it
}
}
}
private fun parseLocation(location: SendLocation, dialog: Dialog) {
botDefinition.userLocationStory?.let { definition ->
definition.mainIntent().let {
dialog.state.currentIntent = it
}
}
}
private fun parseChoice(choice: SendChoice, dialog: Dialog) {
botDefinition.findIntent(choice.intentName, choice.applicationId).let { intent ->
// restore state if it's possible (old dialog choice case)
if (intent != Intent.unknown) {
// TODO use story id
val previousIntentName = choice.previousIntent()
if (previousIntentName != null) {
val previousStory = botDefinition.findStoryDefinition(previousIntentName, choice.applicationId)
if (previousStory != botDefinition.unknownStory && previousStory.supportIntent(intent)) {
// the previous intent is a primary intent that support the new intent
val storyDefinition = botDefinition.findStoryDefinition(choice.intentName, choice.applicationId)
if (storyDefinition == botDefinition.unknownStory) {
// the new intent is a secondary intent, may be we need to create a intermediate story
val currentStory = dialog.currentStory
if (currentStory == null || !currentStory.supportIntent(intent) || !currentStory.supportIntent(botDefinition.findIntent(previousIntentName, choice.applicationId))) {
dialog.stories.add(Story(previousStory, intent))
}
}
}
}
}
dialog.state.currentIntent = intent
}
}
private fun loadProfileIfNotSet(connectorData: ConnectorData, action: Action, userTimeline: UserTimeline, connector: TockConnectorController) {
with(userTimeline) {
val persistProfile = connector.connector.persistProfileLoaded
if (!persistProfile || !userState.profileLoaded) {
val pref = connector.loadProfile(connectorData, userTimeline.playerId)
if (pref != null) {
if (persistProfile) {
userState.profileLoaded = true
userState.profileRefreshed = true
userPreferences.fillWith(pref)
} else {
userPreferences.refreshWith(pref)
}
}
} else if (!userState.profileRefreshed) {
userState.profileRefreshed = true
val pref = connector.refreshProfile(connectorData, userTimeline.playerId)
if (pref != null) {
userPreferences.refreshWith(pref)
}
}
action.state.testEvent = userPreferences.test
}
}
fun markAsUnknown(sendSentence: SendSentence, userTimeline: UserTimeline) {
nlp.markAsUnknown(sendSentence, userTimeline, botDefinition)
}
override fun toString(): String {
return "$botDefinition - ${configuration.name}"
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy