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

Rewrite Scenario DSL #139

Merged
merged 18 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
28 changes: 13 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,19 @@ JAICF is a comprehensive enterprise-level framework from [Just AI](https://just-
[![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin)

```kotlin
object HelloWorldScenario: Scenario() {
init {
state("main") {
activators {
event(AlexaEvent.LAUNCH)
intent(DialogflowIntent.WELCOME)
regex("/start")
}

action {
reactions.run {
sayRandom("Hi!", "Hello there!")
say("How are you?")
telegram?.image("https://somecutecats.com/cat.jpg")
}
val HelloWorldScenario = Scenario {
state("main") {
activators {
event(AlexaEvent.LAUNCH)
intent(DialogflowIntent.WELCOME)
regex("/start")
}

action {
reactions.run {
sayRandom("Hi!", "Hello there!")
say("How are you?")
telegram?.image("https://somecutecats.com/cat.jpg")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class JaicpTestChannel(
factory: JaicpNativeChannelFactory
) : HttpBotChannel {

constructor(scenario: Scenario, factory: JaicpNativeChannelFactory) : this(BotEngine(scenario.model), factory)
constructor(scenario: Scenario, factory: JaicpNativeChannelFactory) : this(BotEngine(scenario), factory)

private val channel = factory.create(botApi)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
package com.justai.jaicf.channel.jaicp

import com.justai.jaicf.builder.Scenario
import com.justai.jaicf.context.ActionContext
import com.justai.jaicf.model.scenario.Scenario

object ScenarioFactory {
fun echo() = object : Scenario() {
init {
fallback { reactions.say("You said: ${request.input}") }
}
fun echo() = Scenario {
fallback { reactions.say("You said: ${request.input}") }
}

fun echoWithAction(block: ActionContext<*, *, *>.() -> Unit) = object : Scenario() {
init {
fallback {
block()
}
fun echoWithAction(block: ActionContext<*, *, *>.() -> Unit) = Scenario {
fallback {
block()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ internal class JaicpNativeChannelTests : JaicpBaseTest() {

private val echoBot = BotEngine(echoWithAction {
reactions.say("You said: ${request.input} from ${reactions::class.simpleName}")
}.model)
})

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.justai.jaicf.channel.jaicp.logging

import com.justai.jaicf.BotEngine
import com.justai.jaicf.activator.regex.RegexActivator
import com.justai.jaicf.builder.Scenario
import com.justai.jaicf.channel.http.HttpBotRequest
import com.justai.jaicf.channel.http.asHttpBotRequest
import com.justai.jaicf.channel.jaicp.*
Expand Down Expand Up @@ -32,7 +33,7 @@ internal class JaicpConversationLoggerTest : JaicpBaseTest() {
): JaicpLogModel = super.createLog(req, ctx, session).also { actLog = it }
}
private val spyLogger = spyk(conversationLogger)
private val echoBot = BotEngine(ScenarioFactory.echo().model, conversationLoggers = arrayOf(spyLogger))
private val echoBot = BotEngine(ScenarioFactory.echo(), conversationLoggers = arrayOf(spyLogger))

@Test
fun `001 logging should set log model with session id for new user`() {
Expand Down Expand Up @@ -71,17 +72,17 @@ internal class JaicpConversationLoggerTest : JaicpBaseTest() {

@Test
fun `004 logging should start new session`() {
val scenario = object : Scenario() {
init {
fallback { reactions.say("You said: ${request.input}") }
state("sid") {
activators { regex("start session") }
action { reactions.chatapi?.startNewSession() }
}
val scenario = Scenario {

state("sid") {
activators { regex("start session") }
action { reactions.chatapi?.startNewSession() }
}

fallback { reactions.say("You said: ${request.input}") }
}
val bot =
BotEngine(scenario.model, conversationLoggers = arrayOf(spyLogger), activators = arrayOf(RegexActivator))
BotEngine(scenario, conversationLoggers = arrayOf(spyLogger), activators = arrayOf(RegexActivator))
val channel = JaicpTestChannel(bot, ChatApiChannel)

channel.process(request.withQuery("Hello!").withClientId(testNumber))
Expand All @@ -105,17 +106,17 @@ internal class JaicpConversationLoggerTest : JaicpBaseTest() {

@Test
fun `005 logging should end current session, next request should go to new session`() {
val scenario = object : Scenario() {
init {
fallback { reactions.say("You said: ${request.input}") }
state("sid") {
activators { regex("end session") }
action { reactions.chatapi?.endSession() }
}
val scenario = Scenario {

state("sid") {
activators { regex("end session") }
action { reactions.chatapi?.endSession() }
}

fallback { reactions.say("You said: ${request.input}") }
}
val bot =
BotEngine(scenario.model, conversationLoggers = arrayOf(spyLogger), activators = arrayOf(RegexActivator))
BotEngine(scenario, conversationLoggers = arrayOf(spyLogger), activators = arrayOf(RegexActivator))
val channel = JaicpTestChannel(bot, ChatApiChannel)

channel.process(request.withQuery("Hello!").withClientId(testNumber))
Expand Down
18 changes: 11 additions & 7 deletions core/src/main/kotlin/com/justai/jaicf/BotEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,31 @@ package com.justai.jaicf
import com.justai.jaicf.activator.ActivationContext
import com.justai.jaicf.activator.Activator
import com.justai.jaicf.activator.ActivatorFactory
import com.justai.jaicf.activator.selection.ActivationSelector
import com.justai.jaicf.activator.catchall.CatchAllActivator
import com.justai.jaicf.activator.event.BaseEventActivator
import com.justai.jaicf.activator.intent.BaseIntentActivator
import com.justai.jaicf.activator.selection.ActivationSelector
import com.justai.jaicf.activator.strict.ButtonActivator
import com.justai.jaicf.api.BotApi
import com.justai.jaicf.api.BotRequest
import com.justai.jaicf.context.*
import com.justai.jaicf.context.BotContext
import com.justai.jaicf.context.ProcessContext
import com.justai.jaicf.context.RequestContext
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.ConversationLogger
import com.justai.jaicf.logging.LoggingContext
import com.justai.jaicf.logging.Slf4jConversationLogger
import com.justai.jaicf.model.scenario.ScenarioModel
import com.justai.jaicf.model.scenario.Scenario
import com.justai.jaicf.reactions.Reactions
import com.justai.jaicf.reactions.ResponseReactions
import com.justai.jaicf.slotfilling.*

/**
* Default [BotApi] implementation.
* You can use it passing the [ScenarioModel] of your bot, [BotContextManager] that manages the bot's state data and an array of [ActivatorFactory]. See params description below.
* You can use it passing the [Scenario] of your bot, [BotContextManager] that manages the bot's state data and an array of [ActivatorFactory]. See params description below.
*
* Here is an example of usage:
*
Expand All @@ -38,7 +40,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 scenario bot scenario. 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.
Expand All @@ -54,14 +56,16 @@ import com.justai.jaicf.slotfilling.*
* @see ConversationLogger
*/
class BotEngine(
val model: ScenarioModel,
scenario: Scenario,
val defaultContextManager: BotContextManager = InMemoryBotContextManager,
activators: Array<ActivatorFactory> = emptyArray(),
private val activationSelector: ActivationSelector = ActivationSelector.default,
private val slotReactor: SlotReactor? = null,
private val conversationLoggers: Array<ConversationLogger> = arrayOf(Slf4jConversationLogger())
) : BotApi, WithLogger {

val model = scenario.scenario

private val activators = activators.map { it.create(model) }.addBuiltinActivators()

private fun List<Activator>.addBuiltinActivators(): List<Activator> {
Expand All @@ -87,7 +91,7 @@ class BotEngine(
* @see BotHook
*/
val hooks = BotHookHandler().also { handler ->
handler.actions.putAll(model.hooks)
handler.actions.putAll(model.hooks.groupBy { it.klass }.mapValues { it.value.toMutableList() })
}

override fun process(
Expand Down
16 changes: 11 additions & 5 deletions core/src/main/kotlin/com/justai/jaicf/activator/BaseActivator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,18 @@ abstract class BaseActivator(private val model: ScenarioModel) : Activator {

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")

val currentPath = StatePath.parse(currentState)
val availableStates = mutableListOf(currentPath.toString()).apply {
if (!isModal) addAll(currentPath.parents.reversedArray())

val allStatesBack = listOf(currentPath.toString()) + currentPath.parents.reversedArray()

val availableStates = mutableListOf<String>()
for (state in allStatesBack) {
availableStates += state

val isModal = model.states[state]?.modal ?: error("State $state is not registered in model")
if (isModal) {
break
}
}

val transitionsMap = model.transitions.groupBy { it.fromState }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ abstract class EventActivationRule(val matches: (EventActivatorContext) -> Boole

open class EventByNameActivationRule(val event: String): EventActivationRule({ it.event == event})

class AnyEventActivationRule(val except: MutableList<String> = mutableListOf()): EventActivationRule({ it.event !in except })
class AnyEventActivationRule(val except: List<String> = mutableListOf()): EventActivationRule({ it.event !in except })
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ abstract class IntentActivationRule(val matches: (IntentActivatorContext) -> Boo

open class IntentByNameActivationRule(val intent: String): IntentActivationRule({ it.intent == intent })

class AnyIntentActivationRule(val except: MutableList<String> = mutableListOf()): IntentActivationRule({ it.intent !in except })
class AnyIntentActivationRule(val except: List<String> = mutableListOf()): IntentActivationRule({ it.intent !in except })
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.justai.jaicf.builder

import com.justai.jaicf.activator.catchall.CatchAllActivationRule
import com.justai.jaicf.activator.event.AnyEventActivationRule
import com.justai.jaicf.activator.event.EventByNameActivationRule
import com.justai.jaicf.activator.intent.AnyIntentActivationRule
import com.justai.jaicf.activator.intent.IntentByNameActivationRule
import com.justai.jaicf.activator.regex.RegexActivationRule
import com.justai.jaicf.model.activation.ActivationRule

@ScenarioDsl
class ActivationRulesBuilder internal constructor() {
private val rules = mutableListOf<ActivationRule>()

/**
* Registers the provided [ActivationRule].
*/
fun rule(rule: ActivationRule) {
rules += rule
}

internal fun build(): List<ActivationRule> = rules

/**
* Registers catch-all activation rule that handles any request.
* Requires a [com.justai.jaicf.activator.catchall.CatchAllActivator] in the activators' list of your [com.justai.jaicf.api.BotApi] instance.
*
* @see com.justai.jaicf.activator.catchall.CatchAllActivator
* @see com.justai.jaicf.api.BotApi
*/
fun catchAll() = rule(CatchAllActivationRule())

/**
* Registers regex activation rule that handles any text that matches the [pattern].
* Requires a [com.justai.jaicf.activator.regex.RegexActivator] in the activators' list of your [com.justai.jaicf.api.BotApi] instance.
*
* @see com.justai.jaicf.activator.regex.RegexActivator
* @see com.justai.jaicf.api.BotApi
*/
fun regex(pattern: Regex) = rule(RegexActivationRule(pattern.pattern))

/**
* Registers regex activation rule that handles any text that matches the [pattern].
* Requires a [com.justai.jaicf.activator.regex.RegexActivator] in the activators' list of your [com.justai.jaicf.api.BotApi] instance.
*
* @see com.justai.jaicf.activator.regex.RegexActivator
* @see com.justai.jaicf.api.BotApi
*/
fun regex(pattern: String) = regex(pattern.toRegex())

/**
* Registers event activation rule that handles an event with name [event].
* Requires a [com.justai.jaicf.activator.event.EventActivator] in the activators' list of your [com.justai.jaicf.api.BotApi] instance.
*
* @see com.justai.jaicf.activator.event.EventActivator
* @see com.justai.jaicf.api.BotApi
*/
fun event(event: String) = rule(EventByNameActivationRule(event))

/**
* Registers any-event activation rule that handles any event.
* Requires a [com.justai.jaicf.activator.event.EventActivator] in the activators' list of your [com.justai.jaicf.api.BotApi] instance.
*
* @see com.justai.jaicf.activator.event.EventActivator
* @see com.justai.jaicf.api.BotApi
*/
fun anyEvent() = rule(AnyEventActivationRule())

/**
* Registers intent activation rule that handles an intent with name [intent].
* Requires a [com.justai.jaicf.activator.intent.IntentActivator] in the activators' list of your [com.justai.jaicf.api.BotApi] instance.
*
* @see com.justai.jaicf.activator.intent.IntentActivator
* @see com.justai.jaicf.api.BotApi
*/
fun intent(intent: String) = rule(IntentByNameActivationRule(intent))

/**
* Registers any-intent activation rule that handles any intent.
* Requires a [com.justai.jaicf.activator.intent.IntentActivator] in the activators' list of your [com.justai.jaicf.api.BotApi] instance.
*
* @see com.justai.jaicf.activator.intent.IntentActivator
* @see com.justai.jaicf.api.BotApi
*/
fun anyIntent() = rule(AnyIntentActivationRule())

}
33 changes: 33 additions & 0 deletions core/src/main/kotlin/com/justai/jaicf/builder/Scenario.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.justai.jaicf.builder

import com.justai.jaicf.api.BotRequest
import com.justai.jaicf.generic.ChannelTypeToken
import com.justai.jaicf.model.ScenarioModelBuilder
import com.justai.jaicf.model.scenario.Scenario
import com.justai.jaicf.model.state.State
import com.justai.jaicf.model.state.StatePath
import com.justai.jaicf.reactions.Reactions


@ScenarioDsl
fun <B : BotRequest, R : Reactions> Scenario(
channelToken: ChannelTypeToken<B, R>,
body: RootBuilder<B, R>.() -> Unit
): Scenario = object : Scenario {
override val scenario by lazy { RootBuilder(ScenarioModelBuilder(), channelToken).apply(body).buildScenario() }
}

@ScenarioDsl
fun Scenario(
body: RootBuilder<BotRequest, Reactions>.() -> Unit
): Scenario = Scenario(ChannelTypeToken.Default, body)

infix fun Scenario.append(other: Scenario): Scenario = object : Scenario {
override val scenario by lazy {
ScenarioModelBuilder().also {
it.states += State(StatePath.root(), noContext = false, modal = false)
it.append(StatePath.root(), this@append, ignoreHooks = false, exposeHooks = true, propagateHooks = true)
it.append(StatePath.root(), other, ignoreHooks = false, exposeHooks = true, propagateHooks = true)
}.build()
}
}
Loading