
engine.dialog.Story.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.dialog
import ai.tock.bot.definition.Intent
import ai.tock.bot.definition.StoryDefinition
import ai.tock.bot.definition.StoryHandler
import ai.tock.bot.definition.StoryStep
import ai.tock.bot.definition.StoryTag.CHECK_ONLY_SUB_STEPS
import ai.tock.bot.definition.StoryTag.CHECK_ONLY_SUB_STEPS_WITH_STORY_INTENT
import ai.tock.bot.engine.BotBus
import ai.tock.bot.engine.BotRepository
import ai.tock.bot.engine.action.Action
import ai.tock.bot.engine.action.SendChoice
import ai.tock.bot.engine.user.PlayerType
import ai.tock.bot.engine.user.UserTimeline
import ai.tock.shared.error
import mu.KotlinLogging
/**
* A Story is a small unit of conversation about a specific topic.
* It is linked to at least one intent - the [starterIntent].
*/
data class Story(
val definition: StoryDefinition,
val starterIntent: Intent,
internal var step: String? = null,
val actions: MutableList = mutableListOf()
) {
companion object {
private val logger = KotlinLogging.logger {}
}
/**
* The last action of the story.
*/
val lastAction: Action? get() = actions.lastOrNull()
/**
* The last user action of the story.
*/
val lastUserAction: Action? get() = actions.findLast { it.playerId.type == PlayerType.user }
/**
* The current step of the story.
*/
val currentStep: StoryStep<*>? get() = definition.steps.asSequence().mapNotNull { findStep(it) }.firstOrNull()
/**
* True if the story handle metrics and is not a main tracked story
*/
val metricStory get() = definition.metricStory
private fun findStep(step: StoryStep<*>): StoryStep<*>? {
if (step.name == this.step) {
return step
} else {
return step.children.asSequence().mapNotNull { findStep(it) }.firstOrNull()
}
}
private fun findStep(
steps: Collection>,
userTimeline: UserTimeline,
dialog: Dialog,
action: Action,
intent: Intent?
): StoryStep<*>? {
// first level
findStepInTree(steps, userTimeline, dialog, action, intent)?.also {
return it
}
// then iterate on children
steps.forEach { s ->
findStep(s.children, userTimeline, dialog, action, intent)?.also {
return it
}
}
return null
}
private fun findStepInTree(
steps: Collection>,
userTimeline: UserTimeline,
dialog: Dialog,
action: Action,
intent: Intent?
): StoryStep<*>? {
// first level
steps.forEach { s ->
if (s.selectFromAction(userTimeline, dialog, action, intent)) {
// check children
findStepInTree(s.children, userTimeline, dialog, action, intent)?.also {
if (s.selectFromActionAndEntityStepSelection(action, intent) == true) {
return it
}
}
return s
}
}
return null
}
private fun findParentStep(child: StoryStep<*>): StoryStep<*>? =
definition.steps.asSequence().mapNotNull { findParentStep(it, child) }.firstOrNull()
private fun findParentStep(current: StoryStep<*>, child: StoryStep<*>): StoryStep<*>? =
current.takeIf { current.children.any { child.name == it.name } }
?: current.children.asSequence().mapNotNull { findParentStep(it, child) }.firstOrNull()
private fun StoryHandler.sendStartEvent(bus: BotBus): Boolean {
// stops immediately if any startAction returns false
return BotRepository.storyHandlerListeners.all {
try {
it.startAction(bus, this)
} catch (throwable: Throwable) {
logger.error(throwable)
true
}
}
}
private fun StoryHandler.sendEndEvent(bus: BotBus) {
BotRepository.storyHandlerListeners.forEach {
try {
it.endAction(bus, this)
} catch (throwable: Throwable) {
logger.error(throwable)
}
}
}
/**
* Handles a request.
*/
fun handle(bus: BotBus) {
definition.storyHandler.apply {
try {
if (sendStartEvent(bus)) {
handle(bus)
}
} finally {
sendEndEvent(bus)
}
}
}
/**
* What is the probability of this request support?
*/
fun support(bus: BotBus): Double = definition.storyHandler.support(bus)
/**
* Does this story supports the action ?
*/
fun supportAction(userTimeline: UserTimeline, dialog: Dialog, action: Action, intent: Intent): Boolean {
if (supportIntent(intent)) {
return true
}
val checkSteps = if (definition.hasTag(CHECK_ONLY_SUB_STEPS_WITH_STORY_INTENT)) {
currentStep?.supportIntent(intent) == true ||
(currentStep?.children ?: definition.steps)
.any { it.supportIntent(intent) }
} else {
true
}
return if (checkSteps) {
currentStep?.selectFromAction(userTimeline, dialog, action, intent) == true ||
(currentStep?.children
?: if (definition.hasTag(CHECK_ONLY_SUB_STEPS)) emptyList() else definition.steps)
.any { it.selectFromAction(userTimeline, dialog, action, intent) }
} else {
false
}
}
/**
* Does this story supports the intent ?
*/
fun supportIntent(intent: Intent): Boolean =
definition.supportIntent(intent) || currentStep?.supportIntent(intent) == true
/**
* Set the current step form the specified action and new intent.
*/
fun computeCurrentStep(userTimeline: UserTimeline, dialog: Dialog, action: Action, newIntent: Intent?) {
// set current step if necessary
var forced = false
if (action is SendChoice && !dialog.state.hasCurrentSwitchStoryProcess) {
action.step()?.apply {
forced = true
step = this
}
}
// revalidate step
val s = currentStep
this.step = s?.name
// check the children of the step
if (!forced) {
s?.children?.let { findStepInTree(it, userTimeline, dialog, action, newIntent) }?.apply {
forced = true
[email protected] = name
}
}
// reset the step if applicable
if (!forced && newIntent != null &&
(
(s?.intent != null && !s.supportIntent(newIntent)) ||
s?.selectFromActionAndEntityStepSelection(action, newIntent) == false
)
) {
this.step = null
}
// check the step from the intent
if (!forced && this.step == null) {
if (s != null) {
var parent: StoryStep<*>? = s
do {
parent = parent?.let { findParentStep(it) }
parent?.children?.let { findStepInTree(it, userTimeline, dialog, action, newIntent) }?.apply {
[email protected] = name
return
}
} while (parent != null)
}
findStep(definition.steps, userTimeline, dialog, action, newIntent)?.apply {
[email protected] = name
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy