Making a skill for Alice. Alice-ktx

Alice-ktx – this is a library on Kotlinsimplifying development Alice's skills from Yandex.DialogsIn this article we will look at the main features of the library.

Installation

First, add the library to your project's dependencies.

dependencies {
    implementation("io.github.danbeldev:alice-ktx:0.0.3")
}

First skill, Echo Bot

fun main() {
    skill {
        id = "..."
        webServer = ktorWebServer {
            port = 8080
            path = "/alice"
        }

        dispatch {
            message({ message.session.new }) {
                response {
                    text = "Привет!"
                }
            }

            message {
                response {
                    text = message.request.command.toString()
                }
            }
        }
    }.run()message {
    response {
        text = message.request.originalUtterance.toString()
    }
}
}
  • id – Unique skill identifier, read Here.

  • webServer – Application configuration using Ktor.

  • port – The port on which the application will be launched. In this case, port 8080 is used.

  • path – The path where the application will be available. In this case, it is /alice.

message({ message.session.new }) {
    response {
        text = "Привет!"
    }
}

This block of code handles new sessions. If the session is new message.session.newthen the text “Hello!” is sent in response.

message {
    response {
        text = message.request.command.toString()
    }
}

This block of code processes all other messages. The text of the user's request is sent in response.

Middleware

Middleware is code that is activated on every event received from the Alice API.

There are two types of Middleware

  1. Outer scope – called before processing filters (innerMiddleware).

  2. Inner region – called after filters are processed, but before the handler (outerMiddleware).

Middleware should always return null to pass the event to the next middleware/handler. If you want to finish processing the event, you should return Response.

Example

dispatch {
  // ...
  outerMiddleware {
    if(message.session.user?.userId == null)
        response { text = "У вас нет аккаунта в Яндексе." }
    else
        null
  }
  // ...
}

Exception Handling

responseFailure is an extension to Dispatcher that allows you to handle errors that occur when executing requests. It provides the ability to define error handlers for different types of exceptions and conditions.

responseFailure should always return null to pass the event to the next handler. If you want to finish processing the event, you should returnResponse.

Example

responseFailure(ArithmeticException::class) {
    response {
        text = "Произошла арифметическая ошибка"
    }
}
responseFailure {
    response {
        text = "Произошла ошибка"
    }
}
responseFailure({ message.session.new }) {
    response {
        text = "В начале сессии произошла ошибка"
    }
}

Dialog API

To get, download and delete downloaded images and sounds, you need to pass OAuth Token while creating DialogApi.

skill {
    // ...
    dialogApi = ktorYandexDialogApi {
        oauthToken = "..."
    }
    // ...
}.run()

For each Yandex account on Dialogues you can upload no more than 100 МБ pictures and 1 ГБ audio To find out how much space is already taken, use this method.

dialogApi.getStatus()

All available API methods.

interface DialogApi {
    suspend fun getStatus(): Response<Status>

    suspend fun uploadImage(url: String): Response<ImageUpload>
    suspend fun uploadImage(file: File): Response<ImageUpload>
    suspend fun getAllImages(): Response<Images>
    suspend fun deleteImage(id: String): Response<Unit>

    suspend fun uploadSound(file: File): Response<SoundUpload>
    suspend fun getAllSounds(): Response<Sounds>
    suspend fun deleteSound(id: String): Response<Unit>
}

All API methods return a wrapper Response<>.

sealed interface Response<T> {
    data class Failed<T>(val message: String): Response<T>
    data class Success<T>(val data: T): Response<T>
}

State Machine

Not all functionality of a skill can be implemented in a single handler. If you need to get some information from the user in several steps or need to direct him depending on the answer, then you need to use FSM.

First, identify the possible states in your skill.

enum class InfoState {
    SET_NAME,
    SET_AGE,
    SET_INFO
}

Initial State when a new session starts, set the initial state

message({ message.session.new }) {
    state.setState(InfoState.SET_NAME.name)
    response {
        text = "Добро пожаловать в навык, как вас зовут?"
    }
}
  • Условие: Processed when a new session starts.

  • Действие: Sets the state to SET_NAME and asks the user for a name.

After receiving the name from the user, we save it and move on to the next state.

message({ state == InfoState.SET_NAME.name }) {
    val username = message.request.originalUtterance.toString()
    state.updateData("name" to username)
    state.setState(InfoState.SET_AGE.name)
    response {
        text = "Рад познакомиться $username, сколько вам лет?"
    }
}
  • Условие: Processed if the current state SET_NAME.

  • Действие: Saves name in state, sets next state SET_AGEand asks for age.

After receiving the age from the user, we save it and move on to the last state.

message({ state == InfoState.SET_AGE.name }) {
    val age = message.request.originalUtterance.toString()
    state.updateData("age" to age)
    state.setState(InfoState.SET_INFO.name)
    response {
        text = "Супер, расскажите о себе"
    }
}
  • Условие: Processed if the current state SET_AGE.

  • Действие: Keeps age in state, sets next state SET_INFOand requests additional information.

At the final stage, after receiving additional information, we form a final answer and complete the session.

message({state == InfoState.SET_INFO.name}) {
    val info = message.request.originalUtterance.toString()
    val data = state.getData()
    state.clear()
    response {
        text = "Вот что мне удалось узнать\n\nИмя-${data["name"]}\nВозраст-${data["age"]}\nИнформация-$info"
        endSession = true
    }
}
  • Условие: Processed if the current state SET_INFO.

  • Действие: Generates a response text based on the collected information, clears the state, and terminates the session.

    Using a state machine allows you to structure user interactions and manage the information gathering process through sequential steps. This is especially useful for complex scenarios that require multiple interactions and maintaining state between steps.

    Buttons and Albums

    To add a button use the method button.

response {
    text = "Выберите тип"
    SchedulesType.entries.forEach {
        button {
            text = it.name
            payload = mapOf("schedule_type" to it.name)
        }
    }
}

There are three types of albums

response {
    text = "CARD ITEMS LIST"
    cardItemsList {
        header = "HEADER"
        repeat(10) { index ->
            item {
                imageId = IMAGE_ID
                title = "#${index + 1}"
            }
        }
        footer {
            text = "Footer text"
            mediaButton {
                text = "Click"
            }
        }
    }
}
response {
    cardBigImage {
        imageId = IMAGE_ID
        title = "CARD BIG IMAGE"
        mediaButton {
            text = "Open url"
            url = "https://ya.ru"
        }
    }
}
response {
    cardImageGallery {
        repeat(10) { index ->
            item {
                imageId = IMAGE_ID
                title = "#${index + 1}"
            }
        }
    }
}

useful links

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *