# Building a Slack AI Chatbot with Amazon Bedrock Claude and LangChain4j

### Overview

* In the era of **Gen AI**, one of the best ways to leverage **Large Language Models (LLMs)** is through the familiar platform of `Slack`. By creating an **AI** Chatbot as a **Slack App**, we can make it operate with a specified persona, much like interacting with a real person. This post introduces a method to create a **Slack**\-based **AI** chatbot using **Kotlin**, **Spring Boot**, **Amazon Bedrock Claude**, and **LangChain4j**.
    

### Steps

* Request permission to use **Amazon Bedrock Claude 3.5 Sonnet** in your **AWS** Infra
    
* Develop a chatbot program that sends AI responses to user questions in **Slack**
    
* Create a **Slack App** to act as the Chatbot and obtain necessary **Bot User OAuth Token**
    

### Required Slack OAuth Bot Token Scopes

* `app_mentions:read`: Allows the app to read messages where it is mentioned. This is limited to channels where the app has been added.
    
* `channels:history`: Enables the app to view message history in public channels to which it has been added.
    
* `groups:history`: Permits the app to view message history in private channels to which it has been invited.
    
* `im:history`: Allows the app to view the history of direct messages with the app.
    
* `mpim:history`: Enables the app to view the history of multi-person direct messages that include the app.
    
* `chat:write`: Allows the app to send messages, but only in channels or conversations where it has been added.
    
* `files:read`: Permits the app to read files shared in channels or conversations where it has access permissions.
    

### Activating Amazon Bedrock Model Access: Claude 3.5 Sonnet

* To utilize the **LLM**, we'll activate `Claude 3.5 Sonnet` in `Amazon Bedrock`. (Using the **Amazon Bedrock Runtime API** instead of the **Anthropic API** ensures data security even if sensitive company information is passed to the **LLM**.)
    

```bash
Amazon Bedrock Console
→ [Providers]
→ [Anthropic]
→ [Claude 3.5 Sonnet]
→ [Request model access]
→ Claude 3.5 Sonnet: [Available to request]
→ [Request model access]
# Edit model access
→ [Next]
# Review and submit
→ [Submit]
```

### build.gradle.kts

* Create a **Spring Boot**\-based project and add the following libraries:
    

```kotlin
val langChain4jVersion = "0.35.0"
dependencies {
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("dev.langchain4j:langchain4j-core:$langChain4jVersion")
    implementation("dev.langchain4j:langchain4j-bedrock:$langChain4jVersion")
    implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14")
}
```

### Creating JsonConfig

* Create an `ObjectMapper` bean to convert user message **JSON** received from **Slack** to **DTO** and vice versa for **AI** responses:
    

```kotlin
@Configuration
class JsonConfig {

    @Bean("objectMapper")
    @Primary
    fun objectMapper(): ObjectMapper {

        return Jackson2ObjectMapperBuilder
            .json()
            .serializationInclusion(JsonInclude.Include.ALWAYS)
            .failOnEmptyBeans(false)
            .failOnUnknownProperties(false)
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .modulesToInstall(kotlinModule(), JavaTimeModule())
            .build()
    }
}
```

### Creating AmazonBedrockConfig

* Create a **LangChain4j**'s `ChatLanguageModel` interface to use **Claude 3.5 Sonnet** from **Amazon Bedrock**:
    

```kotlin
@Configuration
class AmazonBedrockConfig {

    @Bean("amazonBedrockClaude35SonnetChatLanguageModel")
    fun amazonBedrockClaude35SonnetChatLanguageModel(): ChatLanguageModel {

        return BedrockAnthropicMessageChatModel.builder()
            // Currently only supported in US East (N. Virginia) region
            .region(Region.US_EAST_1)
            .model("anthropic.claude-3-5-sonnet-20240620-v1:0")
            // Adjust based on chatbot personality
            .temperature(0.3)
            .topP(0.3f)
            .maxTokens(200000)
            .build()
    }
}
```

### Creating OkHttpConfig

* Create an `OkHttpClient` bean to send **AI** responses to **Slack**:
    

```kotlin
@Configuration
class OkHttpConfig {

    @Bean("okHttpClient")
    fun okHttpClient(): OkHttpClient {

        return OkHttpClient()
            .newBuilder().apply {
                dispatcher(Dispatcher(Executors.newVirtualThreadPerTaskExecutor()))
                connectionSpecs(
                    listOf(
                        ConnectionSpec.CLEARTEXT,
                        ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
                            .allEnabledTlsVersions()
                            .allEnabledCipherSuites()
                            .build()
                    )
                )
                connectTimeout(10, TimeUnit.SECONDS)
                writeTimeout(10, TimeUnit.SECONDS)
                readTimeout(10, TimeUnit.SECONDS)
            }.build()
    }
}
```

### Creating SlackService

* Develop a **SlackService** to send **LLM** responses back to users:
    

```kotlin
@Service
class SlackService(
    private val okHttpClient: OkHttpClient,
    private val objectMapper: ObjectMapper
) {
    @Async(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
    fun sendMessage(channel: String?, threadId: String? = null, text: String?, botUserOAuthToken: String?) {

        if (channel.isNullOrBlank()) return
        if (text.isNullOrBlank()) return
        if (botUserOAuthToken.isNullOrBlank()) return

        // Output in markdown format
        val requestBodyMap = mutableMapOf(
            "channel" to channel,
            "blocks" to listOf(
                mapOf(
                    "type" to "section",
                    "text" to mapOf(
                        "type" to "mrkdwn",
                        "text" to text
                    )
                )
            ),
            "parse" to "full"
        )
        if (!threadId.isNullOrBlank()) {
            requestBodyMap["thread_ts"] = threadId
        }
        val requestBody = objectMapper.writeValueAsString(requestBodyMap)

        okHttpClient.newCall(
            Request.Builder()
                .url("https://slack.com/api/chat.postMessage")
                .addHeader(
                    "Authorization",
                    "Bearer $botUserOAuthToken"
                )
                .post(requestBody.toRequestBody("application/json; charset=utf-8".toMediaType()))
                .build()
        ).execute().use { response ->
            if (!response.isSuccessful) {
                // Exception handling and logging
            }
        }
    }
}
```

### Creating Slack Event Handler Service

* Create a service to send **LLM** responses to user questions that mention the chatbot:
    

```kotlin
@Service
class FooChatbotService(
    private val objectMapper: ObjectMapper,
    @Qualifier("amazonBedrockClaude35SonnetChatLanguageModel") private val amazonBedrockClaude35SonnetChatLanguageModel: ChatLanguageModel,
    private val slackService: SlackService
) {
    @Async(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
    fun processSlackEvent(requestBody: String?) {
    	
        if (requestBody.isNullOrBlank()) return
        
    	 val request: SlackEventCallback = try {
    	     objectMapper.readValue(requestBody, SlackEventCallback::class.java)
    	 } catch(ex: Exception) {
    	 	// Exception handling and logging
    	 	return
    	 }
    	 
    	 // Ignore questions that don`t mention the chatbot
    	 if (!request.event.text.contains("<@${your-slack-bot-user-oauth-token}>")) {
            return
        }

        // Request and obtain LLM response
        val aiMessage: Response<AiMessage> = chatLanguageModel.generate(
            SystemMessage("{your-system-prompt}"),
            UserMessage(request.event.text)
        )
        
        // Send LLM response to the original Slack question thread
        slackService.sendMessage(
            channel = request.event.channel,
            thread = request.event.threadTs ?: request.event.ts,
            text = aiMessage.content().text(),
            token = "{your-slack-bot-user-oauth-token}"
        )
    }
}

data class SlackEventCallback(
    val token: String,
    @JsonProperty("team_id") val teamId: String,
    @JsonProperty("context_team_id") val contextTeamId: String,
    @JsonProperty("context_enterprise_id") val contextEnterpriseId: String?,
    @JsonProperty("api_app_id") val apiAppId: String,
    val event: SlackEvent,
    val type: String,
    @JsonProperty("event_id") val eventId: String,
    @JsonProperty("event_time") val eventTime: Long,
    val authorizations: List<SlackAuthorization>,
    @JsonProperty("is_ext_shared_channel") val isExtSharedChannel: Boolean,
    @JsonProperty("event_context") val eventContext: String
)

data class SlackEvent(
    val user: String,
    val type: String,
    val ts: String,
    @JsonProperty("client_msg_id") val clientMsgId: String?,
    val text: String,
    val team: String?,
    @JsonProperty("thread_ts") val threadTs: String?,
    val blocks: List<SlackBlock>?,
    val channel: String,
    @JsonProperty("event_ts") val eventTs: String,
    @JsonProperty("channel_type") val channelType: String,
    val files: List<SlackFile>?,
    val upload: Boolean?,
    @JsonProperty("display_as_bot") val displayAsBot: Boolean?,
    val subtype: String?
)

data class SlackFile(
    val id: String,
    val created: Long,
    val timestamp: Long,
    val name: String,
    val title: String,
    val mimetype: String,
    val filetype: String,
    @JsonProperty("pretty_type") val prettyType: String,
    val user: String,
    @JsonProperty("user_team") val userTeam: String,
    val editable: Boolean,
    val size: Int,
    val mode: String,
    @JsonProperty("is_external") val isExternal: Boolean,
    @JsonProperty("external_type") val externalType: String,
    @JsonProperty("is_public") val isPublic: Boolean,
    @JsonProperty("public_url_shared") val publicUrlShared: Boolean,
    @JsonProperty("display_as_bot") val displayAsBot: Boolean,
    val username: String,
    @JsonProperty("url_private") val urlPrivate: String,
    @JsonProperty("url_private_download") val urlPrivateDownload: String,
    @JsonProperty("media_display_type") val mediaDisplayType: String,
    @JsonProperty("thumb_64") val thumb64: String?,
    @JsonProperty("thumb_80") val thumb80: String?,
    @JsonProperty("thumb_360") val thumb360: String?,
    @JsonProperty("thumb_360_w") val thumb360W: Int,
    @JsonProperty("thumb_360_h") val thumb360H: Int,
    @JsonProperty("thumb_480") val thumb480: String?,
    @JsonProperty("thumb_480_w") val thumb480W: Int,
    @JsonProperty("thumb_480_h") val thumb480H: Int,
    @JsonProperty("thumb_160") val thumb160: String?,
    @JsonProperty("original_w") val originalW: Int,
    @JsonProperty("original_h") val originalH: Int,
    @JsonProperty("thumb_tiny") val thumbTiny: String?,
    val permalink: String,
    @JsonProperty("permalink_public") val permalinkPublic: String,
    @JsonProperty("has_rich_preview") val hasRichPreview: Boolean,
    @JsonProperty("file_access") val fileAccess: String
)

data class SlackBlock(
    val type: String?,
    @JsonProperty("block_id") val blockId: String?,
    val elements: List<SlackElement>?
)

data class SlackElement(
    val type: String?,
    val elements: List<SlackElementContent?>?
)

data class SlackElementContent(
    val type: String,
    @JsonProperty("user_id") val userId: String? = null,
    val text: String? = null
)

data class SlackAuthorization(
    @JsonProperty("enterprise_id") val enterpriseId: String?,
    @JsonProperty("team_id") val teamId: String,
    @JsonProperty("user_id") val userId: String,
    @JsonProperty("is_bot") val isBot: Boolean,
    @JsonProperty("is_enterprise_install") val isEnterpriseInstall: Boolean
)
```

### Creating Slack Event Handler Controller

* Create a controller to receive messages sent to the chatbot in **Slack** and pass them to the previously created service:
    

```kotlin
@RestController
class FooChatbotController(
    private val objectMapper: ObjectMapper,
    private val fooChatbotService: FooChatbotService
) {
    @PostMapping("/v1/slack/events/foo-chatbot")
    fun onProcessSlackEvent(@RequestBody requestBody: String, request: HttpServletRequest): ResponseEntity<String> {

        val request = try {
            objectMapper.readValue(requestBody, Map::class.java)
        } catch (_: Exception) {
            return ResponseEntity.noContent().build()
        }

        // Used for Request URL verification in Event Subscriptions menu
        request["type"]?.let { type ->
            if (type == "url_verification") {
                request["challenge"]?.let { challenge ->
                    return ResponseEntity.ok().body(challenge.toString())
                }
            }
        }

        // Process all other message events
        fooChatbotService.processSlackEvent(requestBody)

        return ResponseEntity.ok("OK")
    }
}
```

### Creating a Slack App

* The chatbot application is now complete. The final step is to create a dedicated **App** in **Slack** for the chatbot. Don't forget to securely store and inject the generated **Bot User OAuth Token** into your application.
    

```bash
Visit https://api.slack.com/apps
 
# Your Apps
→ [Create an App]
 
# Create an app
→ [From an app manifest]
# Pick a workspace to develop your app
→ Select: {your-workspace}
→ [Next]
# Enter app manifest below
→ JSON: (Paste the following content)
{
    "display_information": {
        "name": "{your-chatbot-name}"
    },
    "features": {
        "app_home": {
            "home_tab_enabled": false,
            "messages_tab_enabled": true,
            "messages_tab_read_only_enabled": false
        },
        "bot_user": {
            "display_name": "{your-chatbot-name}",
            "always_online": true
        }
    },
    "oauth_config": {
        "scopes": {
            "bot": [
                "app_mentions:read",
                "channels:history",
                "groups:history",
                "im:history",
                "mpim:history",
                "chat:write",
                "files:read"
            ]
        }
    },
    "settings": {
        "event_subscriptions": {
            "request_url": "https://{your-domain}/v1/slack/events/foo-chatbot",
            "bot_events": [
                "app_mention",
                "message.channels",
                "message.groups",
                "message.im",
                "message.mpim"
            ]
        },
        "org_deploy_enabled": false,
        "socket_mode_enabled": false,
        "token_rotation_enabled": false
    }
}
→ [Next]
# Review summary & create your app
→ [Create]
 
# Basic Information
→ [Install to Workspace]
→ [Allow]
 
# OAuth & Permissions
→ Copy the Bot User OAuth Token content
{your-slack-bot-user-oauth-token}
 
# Event Subscriptions
→ Request URL: [Retry]
→ [Save Changes]
```

### References

* [Slack - Events API](https://api.slack.com/apis/events-api)
