Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable step up and implement ranking #66

Merged
merged 5 commits into from
Oct 5, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ class CailaIntentActivator(

override fun recogniseIntent(botContext: BotContext, request: BotRequest): List<IntentActivatorContext> {
val results = client.analyze(request.input) ?: return emptyList()
return results.inference.variants.map { CailaIntentActivatorContext(results, it) }
return results.inference.variants.filter {
it.confidence >= settings.confidenceThreshold
}.map { CailaIntentActivatorContext(results, it) }
}

class Factory(private val settings: CailaNLUSettings) : ActivatorFactory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.justai.jaicf.api.BotRequest
import com.justai.jaicf.context.ActivatorContext
import com.justai.jaicf.context.BotContext
import com.justai.jaicf.model.activation.Activation
import com.justai.jaicf.model.activation.selection.ActivationSelector
import com.justai.jaicf.model.scenario.ScenarioModel
import com.justai.jaicf.reactions.Reactions
import com.justai.jaicf.slotfilling.SlotFillingResult
Expand All @@ -24,8 +25,9 @@ class AlexaActivator private constructor(
override fun canHandle(request: BotRequest) =
intentActivator.canHandle(request) || eventActivator.canHandle(request)

override fun activate(botContext: BotContext, request: BotRequest): Activation? {
return intentActivator.activate(botContext, request) ?: eventActivator.activate(botContext, request)
override fun activate(botContext: BotContext, request: BotRequest, selector: ActivationSelector): Activation? {
return intentActivator.activate(botContext, request, selector)
?: eventActivator.activate(botContext, request, selector)
}

override fun fillSlots(
Expand Down
7 changes: 5 additions & 2 deletions core/src/main/kotlin/com/justai/jaicf/BotEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import com.justai.jaicf.context.manager.BotContextManager
import com.justai.jaicf.context.manager.InMemoryBotContextManager
import com.justai.jaicf.helpers.logging.WithLogger
import com.justai.jaicf.hook.*
import com.justai.jaicf.logging.Slf4jConversationLogger
import com.justai.jaicf.logging.ConversationLogger
import com.justai.jaicf.logging.LoggingContext
import com.justai.jaicf.logging.Slf4jConversationLogger
import com.justai.jaicf.model.activation.Activation
import com.justai.jaicf.model.activation.selection.ActivationSelector
import com.justai.jaicf.model.scenario.ScenarioModel
import com.justai.jaicf.reactions.Reactions
import com.justai.jaicf.reactions.ResponseReactions
Expand All @@ -40,6 +41,7 @@ import com.justai.jaicf.slotfilling.*
* @param model bot scenario model. Every bot should serve some scenario that implements a business logic of the bot.
* @param defaultContextManager the default manager that manages a bot's context during the request execution. Can be overriden by the channel itself fot every user's request.
* @param activators an array of used activator that can handle a request. Note that an order is matter: lower activators won't be called if top-level activator handles a request and a corresponding state is found in scenario.
* @param activationSelector a selector that is used for selecting the most relevant [ActivationSelector] from all possible.
* @param slotReactor an entity to react to filling specified slot.
* @param conversationLoggers an array conversation loggers, all of which will log conversation information after request is processed.
*
Expand All @@ -55,6 +57,7 @@ class BotEngine(
val model: ScenarioModel,
val defaultContextManager: BotContextManager = InMemoryBotContextManager,
activators: Array<ActivatorFactory>,
private val activationSelector: ActivationSelector = ActivationSelector.default,
private val slotReactor: SlotReactor? = null,
private val conversationLoggers: Array<ConversationLogger> = arrayOf(Slf4jConversationLogger())
) : BotApi, WithLogger {
Expand Down Expand Up @@ -218,7 +221,7 @@ class BotEngine(
): ActivationContext? {

activators.filter { it.canHandle(request) }.forEach { a ->
val activation = a.activate(botContext, request)
val activation = a.activate(botContext, request, activationSelector)
if (activation != null) {
if (activation.state != null) {
return ActivationContext(a, activation)
Expand Down
5 changes: 4 additions & 1 deletion core/src/main/kotlin/com/justai/jaicf/activator/Activator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.justai.jaicf.api.BotRequest
import com.justai.jaicf.context.ActivatorContext
import com.justai.jaicf.context.BotContext
import com.justai.jaicf.model.activation.Activation
import com.justai.jaicf.model.activation.selection.ActivationSelector
import com.justai.jaicf.model.scenario.ScenarioModel
import com.justai.jaicf.reactions.Reactions
import com.justai.jaicf.slotfilling.SlotFillingResult
Expand Down Expand Up @@ -54,6 +55,7 @@ interface Activator {
*
* @param botContext a current user's [BotContext]
* @param request a current [BotRequest]
* @param selector a given [ActivationSelector]
* @return [Activation] that contains an optional state of scenario and [com.justai.jaicf.context.ActivatorContext] or null if activator cannot handle a request at all.
*
* @see BotContext
Expand All @@ -62,7 +64,8 @@ interface Activator {
*/
fun activate(
botContext: BotContext,
request: BotRequest
request: BotRequest,
selector: ActivationSelector
): Activation?


Expand Down
48 changes: 14 additions & 34 deletions core/src/main/kotlin/com/justai/jaicf/activator/BaseActivator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import com.justai.jaicf.context.ActivatorContext
import com.justai.jaicf.context.BotContext
import com.justai.jaicf.model.activation.Activation
import com.justai.jaicf.model.activation.ActivationRule
import com.justai.jaicf.model.activation.selection.ActivationSelector
import com.justai.jaicf.model.scenario.ScenarioModel
import com.justai.jaicf.model.state.StatePath
import com.justai.jaicf.model.transition.Transition

/**
Expand All @@ -16,40 +16,17 @@ import com.justai.jaicf.model.transition.Transition
* @see com.justai.jaicf.activator.event.BaseEventActivator
* @see com.justai.jaicf.activator.intent.BaseIntentActivator
*/
abstract class BaseActivator(model: ScenarioModel) : Activator {
abstract class BaseActivator(private val model: ScenarioModel) : Activator {

private val transitions = model.transitions.groupBy { it.fromState }

override fun activate(botContext: BotContext, request: BotRequest): Activation? {
val matcher = provideRuleMatcher(botContext, request)
override fun activate(botContext: BotContext, request: BotRequest, selector: ActivationSelector): Activation? {
val transitions = generateTransitions(botContext)
val matcher = provideRuleMatcher(botContext, request)

val activations = transitions.mapNotNull { transition ->
matcher.match(transition.rule)?.let { Activation(transition.toState, it) }
}.toList()

if (activations.isEmpty()) return null
return selectActivation(botContext, activations)
}
matcher.match(transition.rule)?.let { transition to it }
}

/**
* This method is used for selection the most relevant activation.
*
* Default implementation at first tries to select an activation with the greatest confidence from all children
* of the current state, then from all siblings (including current state),
* then from all siblings of the parent (including the parent), and so on.
*
* @param botContext a current user's [BotContext]
* @param activations list of all available activations
* @return the most relevant [Activation] in terms of certain implementation of [BaseActivator]
*
* @see Activation
*/
protected open fun selectActivation(botContext: BotContext, activations: List<Activation>): Activation {
val first = StatePath.parse(activations.first().state!!)
return activations.takeWhile {
StatePath.parse(it.state!!).parent == first.parent
}.maxBy { it.context.confidence }!!
return selector.selectActivation(botContext, activations)
}

/**
Expand Down Expand Up @@ -81,9 +58,12 @@ abstract class BaseActivator(model: ScenarioModel) : Activator {
override fun match(rule: ActivationRule) = (rule as? R)?.let(matcher)
}

private fun generateTransitions(botContext: BotContext): Sequence<Transition> {
val currentState = StatePath.parse(botContext.dialogContext.currentContext).resolve(".")
val states = generateSequence(currentState) { if (it.toString() == "/") null else it.stepUp() }
return states.flatMap { transitions[it.toString()]?.asSequence() ?: emptySequence() }
private fun generateTransitions(botContext: BotContext): List<Transition> {
val currentState = botContext.dialogContext.currentContext
val isModal = currentState != "/" && model.states[currentState]?.modal
?: error("State $currentState is not registered in model")
return model.transitions.filter {
it.fromState == currentState || it.toState == currentState || (isModal && it.fromState == "/")
}.distinct()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ import com.justai.jaicf.model.scenario.ScenarioModel
*
* @see BaseActivator
*/
open class BaseIntentActivator(
model: ScenarioModel
) : BaseActivator(model), IntentActivator {
open class BaseIntentActivator(model: ScenarioModel) : BaseActivator(model), IntentActivator {
override val name = "baseIntentActivator"

override fun canHandle(request: BotRequest) = request.hasIntent()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.justai.jaicf.model.activation.selection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this interface and its implementations should be placed to model?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can move it to com.justai.jaicf.activator.selection, is it okay?


import com.justai.jaicf.context.ActivatorContext
import com.justai.jaicf.context.BotContext
import com.justai.jaicf.model.activation.Activation
import com.justai.jaicf.model.transition.Transition

/**
* A selector, that is used for selecting the most relevant [Activation] from all possible.
*
* @see [com.justai.jaicf.activator.Activator]
* @see [ContextFirstActivationSelector]
* @see [ContextRankingActivationSelector]
*/
interface ActivationSelector {

/**
* Receives a list of all possible transitions and selects the most relevant one.
*
* @param botContext a current [BotContext].
* @param activations a list of pairs of [Transition] and [ActivatorContext] to choose from.
*
* @return [Activation] that is built from the most relevant transition and context,
* or `null` if no [Activation] should be selected.
*/
fun selectActivation(
botContext: BotContext,
activations: List<Pair<Transition, ActivatorContext>>
): Activation?

companion object {
val default = ContextFirstActivationSelector()
}
}

val Transition.isFromRoot get() = fromState == "/"
fun Transition.isFrom(from: String) = fromState == from
fun Transition.isTo(to: String) = toState == to
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.justai.jaicf.model.activation.selection

import com.justai.jaicf.context.ActivatorContext
import com.justai.jaicf.context.BotContext
import com.justai.jaicf.model.activation.Activation
import com.justai.jaicf.model.transition.Transition

/**
* Selects the most relevant activation, based on context specificity first and then confidence.
*
* Tries to select the most confident transition from current state to some of its child,
* if no children available tries to stay in the same state, otherwise tries to select best transition
* available from scenario root.
*/
class ContextFirstActivationSelector : ActivationSelector {
override fun selectActivation(
botContext: BotContext,
activations: List<Pair<Transition, ActivatorContext>>
): Activation? {
val current = botContext.dialogContext.currentContext

val toChildren = activations.filter { it.first.isFrom(current) }.maxBy { it.second.confidence }
val toCurrent = activations.filter { it.first.isTo(current) }.maxBy { it.second.confidence }
val fromRoot = activations.filter { it.first.isFromRoot }.maxBy { it.second.confidence }

val best = toChildren ?: toCurrent ?: fromRoot
return best?.let { Activation(it.first.toState, it.second) }
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.justai.jaicf.model.activation.selection

import com.justai.jaicf.context.ActivatorContext
import com.justai.jaicf.context.BotContext
import com.justai.jaicf.model.activation.Activation
import com.justai.jaicf.model.transition.Transition

/**
* Selects the most relevant activation, based on confidence weighting.
*
* Default implementation multiplies activation confidence by a factor that is different
* for different layers of scenario graph. More specific transition means greater factor.
* Specificity is ordered as follows:
* 1. Children of the current state;
* 2. The current state itself;
* 3. States available from the root of scenario.
*/
open class ContextRankingActivationSelector : ActivationSelector {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not to use this implementation as default selector instead of ContextFirstActivationSelector? Looks like it covers the context-first selector cases.

override fun selectActivation(
botContext: BotContext,
activations: List<Pair<Transition, ActivatorContext>>
): Activation? {
return activations.maxBy {
calculateScore(botContext, it.first, it.second)
}?.let { Activation(it.first.toState, it.second) }
}


/**
* Calculates the score of the given [Transition] and given [ActivatorContext].
* One can override this method and implement its own scoring mechanism.
*/
protected open fun calculateScore(
botContext: BotContext,
transition: Transition,
context: ActivatorContext
): Float {
val current = botContext.dialogContext.currentContext

val factor = when {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these magic numbers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just by trial-and-error. We have only three sets of states, so we can define constants for them instead of calculating some penalty at runtime

transition.isFrom(current) -> 1f
transition.isTo(current) -> 0.8f
transition.isFromRoot -> 0.6f
else -> 0f
}

return context.confidence * factor
}
}