tl;dr: code is here https://github.com/gasabr/spring-telegrambot
Motivation
There are a lot of (AI) conversation bots right now. Telegram is one of the most popular messaging platforms in the world comparable with WhatsApp and others. Java Spring Framework is being used in a lot of projects, so combining them might be useful for many people building online conversation who do not want to set up virtual environment or download npm packages:)
IMO chatbot provides questionable UX, but it’s so common nowadays, I won’t continue this line of thought. The examples in the article would focus on telegram, but the idea can be used to implement conversations with any chat platform.
Idea
This approach leverages Spring State Machine (SSM) to manage conversation flows in a Telegram bot. By mapping chat interactions to SSM concepts, we create a robust architecture for handling complex dialogs. Key benefits include:
- Structured conversation management
- Intuitive handling of user inputs via event-driven transitions
- Input validation using guard conditions
- Automated actions on state entry/exit
- Built-in error handling and state persistence
While demonstrated with Telegram and Spring, these concepts can be adapted to other platforms. The goal is to provide a scalable, maintainable architecture for conversation bots that can handle complex interactions while remaining flexible.
Implementation
- Creating telegram bean
- Getting updates
- Mapping conversation to state machine abstractions
- Sending updates to state machine
- Sending messages back to the user
- Error handling
Creating telegram bean
I’m going to use pengrad telegram library since it has complete support for the telegram api. First, we need to create an instance of the TelegramBot class which is responsible for getting updates and sending responses. Here is the configuration and the code
telegram:
api-token: ${TELEGRAM_API_TOKEN}
@ConfigurationProperties(prefix = "telegram")
data class TelegramConfigurationProperties @ConstructorBinding constructor(
val apiToken: String,
)
@Configuration
@EnableConfigurationProperties(TelegramConfigurationProperties::class)
class TelegramConfiguration {
@Bean
fun telegramBot(properties: TelegramConfigurationProperties) =
TelegramBot(properties.apiToken)
}
In the actual code you might want to add enabled
flag or a channel id, this is
the simplest possible configuration.
Getting updates
Once we got the bot, getting updates is quite straightforward, I opted to
implement it in a separate component to simplify adding support for webhooks
later. Other than that it would enough to use telegramBot.setUpdatesListener
and log the message for now.
@Component
class TelegramBotSetup(
private val telegramBot: TelegramBot,
) {
companion object {
val logger = LoggerFactory.getLogger(TelegramBotSetup::class.java)!!
}
@PostConstruct
fun configureBot() {
logger.info("Configuring telegram bot to use pulling.")
telegramBot.setUpdatesListener {
logger.info("Got some updates.")
it.forEach { upd ->
logger.info("Got an update: `$upd`.")
}
return@setUpdatesListener UpdatesListener.CONFIRMED_UPDATES_ALL
}
}
}
Implementing conversation logic
In this toy example I’m going to implement 2 functions for the bot:
- get user’s name and greet them
- echo what was sent to it in the upper case
Those are not useful by any means, but they would showcase everything one would need to create a proper bot:
- multiple consecutive phases of the conversation;
- sub-conversations triggered by commands;
- error handling;
- returning to the initial state when conversation is over;
![[Pasted image 20240610072027.png]]
So to convert telegram conversation to the spring state machine I would use:
- Initial & end states to create a conversation, one user might have multiple such conversations;
- Guards to allow only a certain type of messages to trigger the transition between states;
- Inherited or nested state machines to handle branches of the conversation
- External state to store the send context between the states, since modeling it with types would be quite hard;
- On entry state actions to send messages back to the user; Configuration of the state machine above would look something like this in code (complete working example is posted on github)
@Configuration
@EnableStateMachine
class StateMachineConfig(
private val telegramBot: TelegramBot,
) : StateMachineConfigurerAdapter<States, Events>() {
override fun configure(states: StateMachineStateConfigurer<States, Events>) {
super.configure(states)
states.withStates()
.initial(States.IDLE) // the state where sm starts when created
.choice(States.GOT_CMD) // phantom state that routes sm based on condition
.states(States.entries.toTypedArray().toMutableSet())
.and()
.withStates()
.parent(States.GOT_CMD) // a way to define branches of the convo
.initial(States.GOT_HELLO_CMD) // start of the branch
.stateEntry(
States.GOT_HELLO_CMD,
CallbackAction(telegramBot) { c, b ->
sendNamePrompt(c, b)
})
.state(States.GOT_NAME, CallbackAction(telegramBot) { c, b ->
sendHelloWorld(c, b)
}) // next state in the branch
.end(States.CONVERSATION_ENDED) // global end of the conversation
}
override fun configure(transitions: StateMachineTransitionConfigurer<States, Events>) {
super.configure(transitions)
transitions
.withExternal()
.source(States.IDLE)
.event(Events.GOT_TEXT)
.guard(AnyCommandGuard())
.target(States.GOT_CMD)
.and()
.withExternal()
.event(Events.GOT_TEXT)
.source(States.GOT_HELLO_CMD)
.target(States.GOT_NAME)
.and()
.withExternal()
.event(Events.GOT_TEXT)
.source(States.GOT_NAME)
.target(States.CONVERSATION_ENDED)
.and()
.withExternal()
.event(Events.GOT_INVALID_INPUT)
.source(States.GOT_NAME)
.target(States.GOT_HELLO_CMD)
.and()
.withChoice()
.source(States.GOT_CMD)
.first(States.GOT_HELLO_CMD, CommandGuard("hello"))
.then(States.GOT_ANOTHER_CMD, CommandGuard("another"))
.last(States.IDLE, CallbackAction(telegramBot) { c, b -> })
}
}
Error handling
How do professionals do it? Telegram itself just resends you a message implying that the last one contained the error, so that would be a good enough tactic for me. To implement that the cleanest way would probably be to return conversation to the previous state which would cause resending of the message, since it is binded to OnStateEntry event. The implementation will be quite straightforward:
class CallbackAction(
private val errorEvent: Events = Events.GOT_INVALID_INPUT,
private val callback: (StateContext<States, Events>) -> Unit,
) : Action<States, Events> {
companion object {
private val logger = LoggerFactory.getLogger(CallbackAction::class.java)
}
override fun execute(context: StateContext<States, Events>) {
try {
callback(context)
} catch (e: Exception) {
logger.error("Got error executing callback: ${e.message}")
sendSmEvent(context.stateMachine, errorEvent)
}
sendSmEvent(context.stateMachine, Events.SENT_RESPONSE)
}
}
sendSmEvent is just a helper to avoid tedious error handling in async functions
private fun sendSmEvent(stateMachine: StateMachine<States, Events>, event: Events) {
val logger = LoggerFactory.getLogger("SmSender")
stateMachine.sendEvent(Mono.just(GenericMessage(event))).subscribe(
{
logger.debug("Sent event={} to the sm", event)
}, { sendingException ->
logger.error("Error when sending event to state machine: $sendingException")
})
}
we catch the exception and return conversation to the previous state with the configured error event and transition.
Conclusion
I hope this article have demonstrated how Spring State Machine can create robust, flexible Telegram bots for complex conversations. This approach offers structured dialog management, easy error handling, and platform independence. While basic, this implementation provides a solid foundation for more advanced chatbots, balancing scalability with maintainability.
Future work
- make the whole thing async with mono
- figure out proper abstractions and make a starter (i have a working concept, will release later)
- add proper persistence for nested state machines via sm-data
- add spring integration pieces, if it would provide a better abstraction over telegram messages
References
- telegram bot api https://docs.python-telegram-bot.org/en/v21.3/
- java telegram api library https://github.com/pengrad/java-telegram-bot-api
- spring state machine docs https://docs.spring.io/spring-statemachine/docs/current/reference/