(.NET) How to Create a Chatbot
What's a chatbot? It's a Space application that communicates with a Space user in its own Chats channel. A minimum viable bot must:
Respond with a list of available commands when a user types
/
(slash) in the channel.Provide at least one command: After a user sends this command to the channel, the bot must do something and then respond with a message.
What will we do
Let's create a first chatbot! Please welcome – the 'Remind me' bot!
Our bot will send a reminder to a user after a given amount of time. For example, if a user sends the remind 60 take a nap
command to the bot, after 60 seconds, the bot will respond with the take a nap
message. Also, our bot will have a command that provides help
when requested.
In this tutorial, we'll go through the entire process of creating a chatbot. You can also download the resulting source code.
Starter kit for creating a chat bot
What will we need along our journey?
We'll write our bot in C# and .NET. There are many IDE's available for building .NET applications, so you can use your preferred IDE. In this tutorial, we'll use JetBrains Rider. | |
As we have learned in the Develop Applications topic, any application must communicate with Space using the Space HTTP API. To make developing Space apps easier, we provide Space SDK for Kotlin and .NET. The SDK contains the HTTP API client that lets you easily authenticate in and communicate with Space. We'll use the Space SDK by adding a NuGet package dependency to our project. | |
A tunnelling service Such a service exposes local servers to the public internet. It will let us run our chatbot locally and access it from Space via a public URL (we'll specify it as the chatbot's endpoint). For example, you can use the ngrok tunnelling service for this purpose. To start working with ngrok, you should download the ngrok client. For our purposes, the free plan for ngrok is enough. |
Step 1. Create an ASP.NET application
Open JetBrains Rider.
In the welcome screen, start creating a new solution with New Solution.
In the list of templates, select ASP.NET Core Web Application.
In Type, select Empty. This template will set up a project with the minimal required configuration.
Specify a solution name and project name, for example
RemindMeBot
, and click Create.That's it! We now have a web application where we can start building our bot.
Step 2. Get the Space SDK
Use the context menu on the RemindMeBot project, and select Manage NuGet Packages. This will open the NuGet tool window in Rider.
Search for the
JetBrains.Space.AspNetCore
package. This package contains helpers to create Space applications, such as the chatbot we are building.Done! Now, we have the Space SDK in our project, and we can start building the 'Remind me' bot.
Step 3. Run the tunneling service
Before we register our chatbot in Space, we have to get a publicly available URL for it. As your development environment is probably located behind NAT, the easiest way to get the URL is to use a tunneling service. In our case, we'll use ngrok.
Download and unzip the ngrok client.
In terminal (on macOS or Linux) or in the command line (on Windows), open the ngrok directory.
By default, our project is configured to run the HTTP server on the port 5001 (you can check this in the
Properties/launchSettings.json
file). Run tunnelling for this port:./ngrok http 5001The ngrok service will start. It will look similar to:
Session Status online Account user@example.com (Plan: Free) Version 3.0.6 Region United States (us) Latency - Web Interface http://127.0.0.1:4040 Forwarding https://98af-94-158-242-146.ngrok.io -> http://localhost:8080 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00Here we are interested in the
Forwarding
line – it contains the public URL. ngrok redirects requests from this URL to our localhost using its tunnelling service. In the example above, the address ishttps://98af-94-158-242-146.ngrok.io
but in your case it will be something else as ngrok generates these random URLs dynamically.Great job! Now, we have a running tunneling service and the public URL of our future chatbot.
Step 4. Register the chatbot in Space
In order Space and chatbot could communicate with each other, we must register the bot in Space.
When developing an application, you must decide on two important things:
Single-org applications are intended only for a single Space organization. A Space user registers and configures a single-org application manually in the Space UI.
Multi-org applications are intended for multiple Space organizations. A multi-org application registers and configures itself in a particular Space instance using API calls.
Since we are just practicing, there is no point in being distracted by the complexities of configuring a multi-org application. Instead, we will register and configure our application using the Space UI. So, we are going to create a single-org application.
Authorization subject: decide how your application should act in Space – on behalf of itself, on behalf of a particular Space user, or both. This determines which authorization flows the application will use.
In our case, a chatbot will send notifications in its own chat channel on befalf of itself. As an OAuth 2.0 flow, we will use the Client Credentials flow. It lets the application authorize in Space using a client ID and a client secret.
To summarize, our chatbot is a single-org application that uses the Client Credentials authorization flow. Now, let's register our bot in Space.
Open your Space instance.
On the main menu, click Extensions and choose Installed.
Click New application.
Give the application a unique name, say,
remind-me-bot
and click Create.On the Overview tab, select Chat bot. This will enable a chat channel for our application.
Open the Authorization tab. We will not change anything on this tab – as our bot is simple and doesn't get any data from Space.
We're on this tab just for you to note how important it is – if your app is supposed to access various Space modules, you should provide it corresponding permissions. Learn more about requesting permissions.
Open the Authentication tab. Note that the Client Credentials Flow is enabled for all applications by default. We need to get the application's Client ID and Client secret. Our chatbot will use them to get a Space access token.
When a user types anything in the chatbot channel, Space sends the user input to the application. So, our next step is to specify the URL of our application endpoint and choose how we will verify requests from Space.
Open the Endpoint tab.
In the Endpoint URI, specify the public URL generated by the tunnelling service for our bot. Let's make this endpoint less generic and add a postfix to the URL, for example
api/space
. So, the final endpoint would behttps://{random_string_from_ngrok}.ngrok.io/api/space
By default, Space recommends using the Public key verification method. Let's leave the default and click Save.
Great job! Now, our bot is registered in Space, we have all required authentication data, and we're ready to start developing our bot.
Step 5. Register a web hook handler
Ready, set, code! We'll start by updating the configuration file in our application. Then, we'll create and register a RemindMeBotHandler
class that can respond to incoming messages from Space, and send responses back.
Edit the
appsettings.json
file in your project.Add a
Space
element to it. The file should look like this:{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "Space": { "ServerUrl": "https://organization.jetbrains.space", "ClientId": "value-of-client-id", "ClientSecret": "value-of-client-secret", "VerifySigningKey": { "IsEnabled": false, "EndpointSigningKey": "value-of-endpoint-signing-key" }, "VerifyHttpBearerToken": { "IsEnabled": false, "BearerToken": "value-of-bearer-token}" }, "VerifyHttpBasicAuthentication": { "IsEnabled": false, "Username": "value-of-username", "Password": "value-of-password" }, "VerifyVerificationToken": { "IsEnabled": false, "EndpointVerificationToken": "value-of-endpoint-verification-token" } } }You will have to make some updates to the values of elements:ServerUrl
is the URL of your Space instance.ClientId
,ClientSecret
are the ID and secret we obtained during application registration.Set the
IsEnabled
option totrue
for the selected authentication method during application registration. Make sure to update other options as well, for example set theEndpointSigningKey
orEndpointVerificationToken
when needed.
Create the
RemindMeBotHandler
class in your project, and add the code:using JetBrains.Space.AspNetCore.Experimental.WebHooks; namespace RemindMeBot; public class RemindMeBotHandler : SpaceWebHookHandler { }Note that theRemindMeBotHandler
class inherits from the Space SDK'sSpaceWebHookHandler
class. It handles a number of things for us:It verifies the Space instance from which our bot receives a request, by taking the message payload and comparing the incoming token with the EndpointVerificationToken configuration value.
It ensures the message payload has not been tampered with, by using the EndpointSigningKey, and calculating the message signature.
Now, let's wire things up. We will update code in
Program.cs
, and register the application helpers for Space.Right after the line that reads
var builder = WebApplication.CreateBuilder(args);
, add the following code:// Space client API builder.Services.AddSingleton<Connection>(provider => new ClientCredentialsConnection( new Uri(builder.Configuration["Space:ServerUrl"]), builder.Configuration["Space:ClientId"], builder.Configuration["Space:ClientSecret"], provider.GetService<IHttpClientFactory>().CreateClient())); builder.Services.AddSpaceClientApi(); // Space webhook handler builder.Services.AddSpaceWebHookHandler<RemindMeBotHandler>(options => builder.Configuration.Bind("Space", options));Some things to note:
AddHttpClient()
registers the .NETHttpClient
that is used by the Space SDK and makes it available as a service.AddSingleton<Connection>(...)
registers a connection to work with Space, using the configuration we created earlier.AddSpaceClientApi()
registers the Space API client and makes it available as a service. Later in this tutorial, we can make use of it to send a chat message to our bot's users.AddSpaceWebHookHandler<RemindMeBotHandler>(...)
registers aRemindMeBotHandler
class as a web hook handler. This class does not exist yet: it will contain the logic of the 'Remind me' bot, which we still have to implement.
Further in the
Program.cs
file, right after the line that readsvar app = builder.Build();
, add the following code:app.MapSpaceWebHookHandler<RemindMeBotHandler>("/api/myapp"); app.MapGet("/", () => "Space app is running.");Things to note:
app.MapSpaceWebHookHandler<RemindMeBotHandler>
makes the 'Remind me' bot available on the web server, on the/api/myapp
path. This has to match the path that was entered during application registration.app.MapGet("/", ...
registers a default response that the server will return when someone accesses the 'Remind me' bot from their browser. It is good practice to add a default response like this to your Space bot.
Done! We have created a
RemindMeBotHandler
class where we can start adding a first command, and registered it using the Space SDK.
Step 6. Create your first command
Let's start with something simple – the help
command that shows hints on how to use our chatbot.
Update the
RemindMeBotHandler
class, and overwrite its code with the following code:using System.Threading.Tasks; using JetBrains.Space.AspNetCore.Experimental.WebHooks; using JetBrains.Space.Client; namespace RemindMeBot; public class RemindMeBotHandler : SpaceWebHookHandler { private readonly ChatClient _chatClient; public RemindMeBotHandler(ChatClient chatClient) { _chatClient = chatClient; } public override async Task HandleMessageAsync(MessagePayload payload) { var messageText = payload.Message.Body as ChatMessageText; if (string.IsNullOrEmpty(messageText?.Text)) return; await HandleHelpAsync(payload); } private async Task HandleHelpAsync(MessagePayload payload) { await _chatClient.Messages.SendMessageAsync( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: ChatMessage.Text("Soon the help will be shown here!")); } }Notes:
When the ASP.NET runtime creates the
RemindMeBotHandler
class, it injects aChatClient
in its constructor. TheChatClient
is stored in a private field, so we can use it later to send chat messages to our users.The
HandleMessageAsync(MessagePayload payload)
method is inherited from the Space SDK, and is called when our bot receives a message from Space. Later on, we will use this method to handle different commands. For now, theHandleHelpAsync
method is invoked whenever a message is received.In the
HandleHelpAsync(MessagePayload payload)
method, we make use of the previously injectedChatClient
through the_chatClient
field.Using the
SendMessageAsync
method, you can send a message to any recipient in Space. Recipients can be chat channels, issue comments, direct messages, and more.We're using the
MessageRecipient.Member()
method to address a user using direct message, and then build theProfileIdentifier
using the ID of the user who invoked thehelp
command.The text of our chat message is created using the
ChatMessage.Text()
method. Messages can be more than just text. They can include complex formatting and even UI elements, like buttons. For now, text is sufficient.
Nice! Now, we have a command that a user can actually try in action.
Step 7. Run the bot
Start the application by pressing Ctrl+F5 or using the menu.
Open your Space instance and find the bot: press Ctrl+K and type its name.
We don't anyhow analyze the commands sent by the user. On any type of request, we respond with the message generated in
HandleHelpAsync
. So, to test our bot, type anything in the chat. You should receive the help message:It's working! Now, let's add the rest of the bot features.
Step 8. Add support for slash commands
Let's make our bot fully functional:
In order to be considered a chatbot, an application must be able to respond with a list of available commands when a user types
/
(slash) in the chat. In this case, the bot receives theListCommandsPayload
type of payload, which we can handle by overriding theHandleListCommandsAsync
method in our bot class.As our bot is called the 'Remind me' bot, it needs a
remind
command that will start the timer and send a notification to the user once the timer completes.
Let's start with listing the available commands. In the
RemindMeBotHandler
class, add the following code:public override async Task<Commands> HandleListCommandsAsync(ListCommandsPayload payload) { return new Commands(new List<CommandDetail> { new CommandDetail("help", "Show this help"), new CommandDetail("remind", "Remind me in N seconds, e.g., to remind in 10 seconds, send 'remind 10'") }); }When a bot receives
ListCommandsPayload
, it can respond withCommands
that returns a list ofCommandDetail
. EachCommandDetail
has a commandname
, and adescription
that will be displayed in the help menu.Space sends
ListCommandsPayload
every time a user presses a key. If it's the/
(slash), Space will show the full list of commands. If it's some other key, Space will find and show only commands containing the key.Now that we can list commands, let's update our
HandleHelpAsync
method and respond with proper help information. Replace the code of theHandleHelpAsync
method:private async Task HandleHelpAsync(MessagePayload payload) { var commands = await HandleListCommandsAsync( new ListCommandsPayload { UserId = payload.UserId }); await _chatClient.Messages.SendMessageAsync( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: ChatMessage.Block( outline: new MessageOutline("Remind me bot help", new ApiIcon("smile")), sections: new List<MessageSectionElement> { MessageSectionElement.MessageSection( header: "List of available commands", elements: new List<MessageElement> { MessageElement.MessageFields( commands.CommandsItems .Select(it => MessageFieldElement.MessageField(it.Name, it.Description)) .ToList<MessageFieldElement>()) }) })); }Here:
We're calling
HandleListCommandsAsync
to get the list of commands. For good measure, we're passing along the user ID as well, so that if we need it in that method, we can use it.The
ChatMessage.Text()
has been replaced withChatMessage.Block()
, so we can return a formatted help message.
We can now check the incoming
MessagePayload
, and determine the intended command. If the text starts with"remind"
, we'll invoke theHandleRemindAsync
method.Update the
HandleMessageAsync
method:public override async Task HandleMessageAsync(MessagePayload payload) { var messageText = payload.Message.Body as ChatMessageText; if (string.IsNullOrEmpty(messageText?.Text)) return; if (messageText.Text.Trim().StartsWith("remind")) { await HandleRemindAsync(payload, messageText); return; } await HandleHelpAsync(payload); }Next, add a new
HandleRemindAsync
method:private async Task HandleRemindAsync(MessagePayload payload, ChatMessageText messageText) { var arguments = messageText.Text.Split(' ', StringSplitOptions.TrimEntries); if (arguments.Length != 2 || !int.TryParse(arguments[1], out var delayInSeconds)) { // We're expecting 2 elements: "remind", "X" // If that's not the case, return help. await HandleHelpAsync(payload); return; } await _chatClient.Messages.SendMessageAsync( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: ChatMessage.Block( outline: new MessageOutline($"I will remind you in {delayInSeconds} seconds", new ApiIcon("smile")), sections: new List<MessageSectionElement>())); Task.Run(async () => { try { await Task.Delay(TimeSpan.FromSeconds(delayInSeconds)); await _chatClient.Messages.SendMessageAsync( recipient: MessageRecipient.Member(ProfileIdentifier.Id(payload.UserId)), content: ChatMessage.Block( outline: new MessageOutline($"Hey! {delayInSeconds} seconds are over!", new ApiIcon("smile")), sections: new List<MessageSectionElement>())); } catch (Exception) { // Since we're using Task.Run to run code outside of the // request context, we want to catch any Exception here // to prevent the server from crashing. } }); }Here:
HandleRemindAsync
validates the command, and invokesHandleHelpAsync
when no valid command arguments are provided. The code checks if there are two elements in the string, and that the second element is a valid integer. When a user typesremind 10
, the second argument has to be an integer10
.A confirmation message is sent to the user.
Task.Run(...)
schedules a delay for the amount of time that was requested, and sends a message to the user when the delay is over.
Let's run the bot one more time and try all bot features:
Typing slash:
The
help
command:The
remind
command:
Great job! We have finished creating our simple bot!