Android — Baking a simple ChatBot in 30 minutes (AIML)

ChatBot, the latest sensation (or just hype!), is believed to be the next paradigm shift in the technology. A conversational assistant will never be boring as it gives new experience each time the user is interacting with it.

A ChatBot need not be always intelligent. It can be as simple as storing user’s notes, reminders, book tickets etc.. After all, satisfying the usecase is critical here.

Now, I’m not going to show you how to sketch a sophisticated ChatBot with NLP and AI, but a simple bot, which can later be trained for your own use case.

Download this APK and feel free to experience the bot which you will be handcrafting in a while.

Assuming that you have the app now, Say “Hi” to the bot!…..
Nice reply isn’t it??. Go ahead and ask “What is you name?” or “Who are you?”
Interesting isn’t it? You can also ask things like — “what is your age?”, “Who created you?” etc. You can also tell your name and you can ask your name back. Feel free to play with it (This simple bot has minimum capabilities, bring out your own creativity for tuning it).

I hope you enjoyed playing with it. Let’s dirty our hands now!

Prerequisite : AIML (Artificial Intelligence Markup Language) basics

Step 1 : Creating chat UI

Create a new Android project and import the following library for chat interface. You can implement your own interface, but importing the following libray, saves a lot of time.

Add the following line under dependencies section in your build.gradle (Module:app) file and sync your gradle settings.

compile 'me.himanshusoni.chatmessageview:chat-message-view:1.0.3'

Let’s now place the components in the xml file. Below your MainActivity’s xml file - activity_main.xml, add the following code.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">

<!--List view for displaying chat messages-->
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/send_message_layout"
android:divider="@null" />
    <!--To type and send the message-->
<LinearLayout
android:id="@+id/send_message_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#ffdddddd"
android:gravity="center_vertical"
android:orientation="horizontal">

<ImageView
android:id="@+id/iv_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/bot" />

<EditText
android:id="@+id/et_message"
android:layout_width="wrap_content"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_height="wrap_content"
android:layout_weight="1" />

<android.support.design.widget.FloatingActionButton
android:id="@+id/btn_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_send_24dp"
android:text="Send" />
</LinearLayout>

</RelativeLayout>

We also need two additional xml files for drawing the chat message of the user and the bot. Under your layout folder, add two additional xml files.

<!--item_mine_message.xml- for user message -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="3dp">

<me.himanshusoni.chatmessageview.ChatMessageView xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/chatMessageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cmv_arrowGravity="start"
app:cmv_arrowMargin="3dp"
app:cmv_arrowPosition="right"
app:cmv_backgroundColor="#88BABABA"
app:cmv_backgroundColorPressed="#FFBABABA"
app:cmv_contentPadding="10dp"
app:cmv_cornerRadius="3dp"
app:cmv_showArrow="true">

<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
</me.himanshusoni.chatmessageview.ChatMessageView>

</LinearLayout>

Note : I’ve created “item_mine_message.xml” for user message and “item_other_message.xml” for bot’s message

<?xml version="1.0" encoding="utf-8"?>
<!--item_other_message.xml- for bot message -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="3dp">

<me.himanshusoni.chatmessageview.ChatMessageView
android:id="@+id/chatMessageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cmv_arrowGravity="start"
app:cmv_arrowPosition="left"
app:cmv_backgroundColor="#8800bcd4"
app:cmv_backgroundColorPressed="#ff00bcd4"
app:cmv_cornerRadius="3dp"
app:cmv_showArrow="true">

<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Helloworld"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
</me.himanshusoni.chatmessageview.ChatMessageView>

</LinearLayout>

To display chats in the UI, we are following singleton pattern. Hence let’s create a POJO class

Note : I’ve named the file as “ChatMessage”

public class ChatMessage {
private boolean isImage, isMine;
private String content;

public ChatMessage(String message, boolean mine, boolean image) {
content = message;
isMine = mine;
isImage = image;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}

public boolean isMine() {
return isMine;
}

public void setIsMine(boolean isMine) {
this.isMine = isMine;
}

public boolean isImage() {
return isImage;
}

public void setIsImage(boolean isImage) {
this.isImage = isImage;
}
}

We need an Adapter for the ListView component to work. So let’s create one. This adapter is tuned to handle two different view types.
Note: Here I’ve named the Adapter as “ChatMessageAdapter”

public class ChatMessageAdapter extends ArrayAdapter<ChatMessage> {
private static final int MY_MESSAGE = 0, OTHER_MESSAGE = 1, MY_IMAGE = 2, OTHER_IMAGE = 3;
    public ChatMessageAdapter(Context context, List<ChatMessage> data) {
super(context, R.layout.item_mine_message, data);
}
    @Override
public int getViewTypeCount() {
// my message, other message, my image, other image
return 4;
}
    @Override
public int getItemViewType(int position) {
ChatMessage item = getItem(position);
        if (item.isMine() && !item.isImage()) return MY_MESSAGE;
else if (!item.isMine() && !item.isImage()) return OTHER_MESSAGE;
else if (item.isMine() && item.isImage()) return MY_IMAGE;
else return OTHER_IMAGE;
}
    @Override
public View getView(int position, View convertView, ViewGroup parent) {
int viewType = getItemViewType(position);
if (viewType == MY_MESSAGE) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_mine_message, parent, false);
            TextView textView = (TextView) convertView.findViewById(R.id.text);
textView.setText(getItem(position).getContent());
        } else if (viewType == OTHER_MESSAGE) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_other_message, parent, false);
            TextView textView = (TextView) convertView.findViewById(R.id.text);
textView.setText(getItem(position).getContent());
} else if (viewType == MY_IMAGE) {
//convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_mine_image, parent, false);
} else {
// convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_other_image, parent, false);
}
convertView.findViewById(R.id.chatMessageView).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(), "onClick", Toast.LENGTH_LONG).show();
}
});
        return convertView;
}
}

All set for ListView? Let’s get back to our MainActivity and implement the below.

  • Declare and define the necessary UI components in the XML
  • Create functions for sending and receiving messages
  • Instruct the ListView to scroll to the latest message
  • Test our chat with static response
public class MainActivity extends AppCompatActivity {


private ListView mListView;
private FloatingActionButton mButtonSend;
private EditText mEditTextMessage;
private ImageView mImageView;
private ChatMessageAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.listView);
mButtonSend = (FloatingActionButton) findViewById(R.id.btn_send);
mEditTextMessage = (EditText) findViewById(R.id.et_message);
mImageView = (ImageView) findViewById(R.id.iv_image);
mAdapter = new ChatMessageAdapter(this, new ArrayList<ChatMessage>());
mListView.setAdapter(mAdapter);

//code for sending the message
mButtonSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String message = mEditTextMessage.getText().toString();
sendMessage(message);
mEditTextMessage.setText("");
mListView.setSelection(mAdapter.getCount() - 1);
}
});
}
private void sendMessage(String message) {
ChatMessage chatMessage = new ChatMessage(message, true, false);
mAdapter.add(chatMessage);
//respond as Helloworld
mimicOtherMessage("HelloWorld");
}

private void mimicOtherMessage(String message) {
ChatMessage chatMessage = new ChatMessage(message, false, false);
mAdapter.add(chatMessage);
}

private void sendMessage() {
ChatMessage chatMessage = new ChatMessage(null, true, true);
mAdapter.add(chatMessage);

mimicOtherMessage();
}

private void mimicOtherMessage() {
ChatMessage chatMessage = new ChatMessage(null, false, true);
mAdapter.add(chatMessage);
}

Now if you run the code, You will be witnessing a chat interface and if you type and send anything, you will get a response as “HelloWorld”.

Step 2 : Importing AIML files

AIML (Artificial Intelligence Markup Language) is an XML-compliant language that’s easy to learn, and makes it possible for you to begin customizing an Alicebot or creating one from scratch within minutes.

The most important units of AIML are:

  • <aiml>: the tag that begins and ends an AIML document
  • <category>: the tag that marks a “unit of knowledge” in an Alicebot’s knowledge base
  • <pattern>: used to contain a simple pattern that matches what a user may say or type to an ALICE bot
  • <template>: contains the response to a user input

The AIML files we are using for customization was written by Dr. Richard S. Wallace, creator of the open-source bot called ALICE. I’ve shared few links at the end of this article for AIML and let’s discuss more about it next time.

Back to our project….

  • Download the following zip file (Hari.zip), extract it and place the folder “Hari” it in your assets folder (app/src/main/assets/<place_it_here>)
  • We also need the library for processing the AIML files. Download this JAR file and place it in your libs folder (app/libs/<place_it_here>)
  • Let’s include the JAR file in to our project by adding the following line “compile files(‘libs/Ab.jar’)” under dependencies in your build.gradle (Module:app) file

The dependencies section of your build.gradle file will look like this.

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.android.support:support-v4:23.3.0'
compile 'com.android.support:cardview-v7:23.3.0'
compile 'com.android.support:design:23.2.1'
compile 'me.himanshusoni.chatmessageview:chat-message-view:1.0.3'
compile files('libs/Ab.jar')

}

Step 3 : Luke Skywalker meets Anakin Skywalker (I mean, the last step!)

First let’s include the class Bot from our ALICE bot JAR package in to our MainActivity.java

public Bot bot;
public static Chat chat;

We need the following functions to read and write the AIML files under our assets folder. Please follow the inline comments on the code block for your understanding. Inside OnCreate(), insert the following snippet.

//checking SD card availablility
boolean a = isSDCARDAvailable();
//receiving the assets from the app directory
AssetManager assets = getResources().getAssets();
File jayDir = new File(Environment.getExternalStorageDirectory().toString() + "/hari/bots/Hari");
boolean b = jayDir.mkdirs();
if (jayDir.exists()) {
//Reading the file
try {
for (String dir : assets.list("Hari")) {
File subdir = new File(jayDir.getPath() + "/" + dir);
boolean subdir_check = subdir.mkdirs();
for (String file : assets.list("Hari/" + dir)) {
File f = new File(jayDir.getPath() + "/" + dir + "/" + file);
if (f.exists()) {
continue;
}
InputStream in = null;
OutputStream out = null;
in = assets.open("Hari/" + dir + "/" + file);
out = new FileOutputStream(jayDir.getPath() + "/" + dir + "/" + file);
//copy file from assets to the mobile's SD card or any secondary memory
copyFile(in, out);
in.close();
in = null;
out.flush();
out.close();
out = null;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
//get the working directory
MagicStrings.root_path = Environment.getExternalStorageDirectory().toString() + "/hari";
System.out.println("Working Directory = " + MagicStrings.root_path);
AIMLProcessor.extension = new PCAIMLProcessorExtension();
//Assign the AIML files to bot for processing
bot = new Bot("Hari", MagicStrings.root_path, "chat");
chat = new Chat(bot);
String[] args = null;
mainFunction(args);

The function definition for the isSDCARDAvailable(),copyFile() and mainFunction() are as follows

//check SD card availability
public static boolean isSDCARDAvailable(){
return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)? true :false;
}
//copying the file
private void copyFile(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[1024];
int read;
while((read = in.read(buffer)) != -1){
out.write(buffer, 0, read);
}
}
//Request and response of user and the bot
public static void mainFunction (String[] args) {
MagicBooleans.trace_mode = false;
System.out.println("trace mode = " + MagicBooleans.trace_mode);
Graphmaster.enableShortCuts = true;
Timer timer = new Timer();
String request = "Hello.";
String response = chat.multisentenceRespond(request);

System.out.println("Human: "+request);
System.out.println("Robot: " + response);
}

Now we need to switch the static response to the bot response. So let’s modify our mButtonSend’s OnClickListener.

mButtonSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String message = mEditTextMessage.getText().toString();
//bot
String response = chat.multisentenceRespond(mEditTextMessage.getText().toString());
if (TextUtils.isEmpty(message)) {
return;
}
sendMessage(message);
mimicOtherMessage(response);
mEditTextMessage.setText("");
mListView.setSelection(mAdapter.getCount() - 1);
}
});

The entire MainActivity’s code will look like the one below.

import android.content.res.AssetManager;
import android.os.Environment;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import org.alicebot.ab.AIMLProcessor;
import org.alicebot.ab.Bot;
import org.alicebot.ab.Chat;
import org.alicebot.ab.Graphmaster;
import org.alicebot.ab.MagicBooleans;
import org.alicebot.ab.MagicStrings;
import org.alicebot.ab.PCAIMLProcessorExtension;
import org.alicebot.ab.Timer;
import com.hariofspades.chatbot.Adapter.ChatMessageAdapter;
import com.hariofspades.chatbot.Pojo.ChatMessage;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {


private ListView mListView;
private FloatingActionButton mButtonSend;
private EditText mEditTextMessage;
private ImageView mImageView;
public Bot bot;
public static Chat chat;
private ChatMessageAdapter mAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.listView);
mButtonSend = (FloatingActionButton) findViewById(R.id.btn_send);
mEditTextMessage = (EditText) findViewById(R.id.et_message);
mImageView = (ImageView) findViewById(R.id.iv_image);
mAdapter = new ChatMessageAdapter(this, new ArrayList<ChatMessage>());
mListView.setAdapter(mAdapter);

mButtonSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String message = mEditTextMessage.getText().toString();
//bot
String response = chat.multisentenceRespond(mEditTextMessage.getText().toString());
if (TextUtils.isEmpty(message)) {
return;
}
sendMessage(message);
mimicOtherMessage(response);
mEditTextMessage.setText("");
mListView.setSelection(mAdapter.getCount() - 1);
}
});
//checking SD card availablility
boolean a = isSDCARDAvailable();
//receiving the assets from the app directory
AssetManager assets = getResources().getAssets();
File jayDir = new File(Environment.getExternalStorageDirectory().toString() + "/hari/bots/Hari");
boolean b = jayDir.mkdirs();
if (jayDir.exists()) {
//Reading the file
try {
for (String dir : assets.list("Hari")) {
File subdir = new File(jayDir.getPath() + "/" + dir);
boolean subdir_check = subdir.mkdirs();
for (String file : assets.list("Hari/" + dir)) {
File f = new File(jayDir.getPath() + "/" + dir + "/" + file);
if (f.exists()) {
continue;
}
InputStream in = null;
OutputStream out = null;
in = assets.open("Hari/" + dir + "/" + file);
out = new FileOutputStream(jayDir.getPath() + "/" + dir + "/" + file);
//copy file from assets to the mobile's SD card or any secondary memory
copyFile(in, out);
in.close();
in = null;
out.flush();
out.close();
out = null;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
//get the working directory
MagicStrings.root_path = Environment.getExternalStorageDirectory().toString() + "/hari";
System.out.println("Working Directory = " + MagicStrings.root_path);
AIMLProcessor.extension = new PCAIMLProcessorExtension();
//Assign the AIML files to bot for processing
bot = new Bot("Hari", MagicStrings.root_path, "chat");
chat = new Chat(bot);
String[] args = null;
mainFunction(args);

}

private void sendMessage(String message) {
ChatMessage chatMessage = new ChatMessage(message, true, false);
mAdapter.add(chatMessage);

//mimicOtherMessage(message);
}

private void mimicOtherMessage(String message) {
ChatMessage chatMessage = new ChatMessage(message, false, false);
mAdapter.add(chatMessage);
}

private void sendMessage() {
ChatMessage chatMessage = new ChatMessage(null, true, true);
mAdapter.add(chatMessage);

mimicOtherMessage();
}

private void mimicOtherMessage() {
ChatMessage chatMessage = new ChatMessage(null, false, true);
mAdapter.add(chatMessage);
}
//check SD card availability
public static boolean isSDCARDAvailable(){
return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)? true :false;
}
//copying the file
private void copyFile(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[1024];
int read;
while((read = in.read(buffer)) != -1){
out.write(buffer, 0, read);
}
}
//Request and response of user and the bot
public static void mainFunction (String[] args) {
MagicBooleans.trace_mode = false;
System.out.println("trace mode = " + MagicBooleans.trace_mode);
Graphmaster.enableShortCuts = true;
Timer timer = new Timer();
String request = "Hello.";
String response = chat.multisentenceRespond(request);

System.out.println("Human: "+request);
System.out.println("Robot: " + response);
}

}

We’re good to go!. Let’s run the app…. and ….Voila! your ChatBot is ready! Serve le Chaud!

Note : Do not forget to add permissions in your Manifest file

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />

If you like this article, please recommend. Feel free to check out the entire project code at Github.

Miscellaneous

Changing the name of the bot

I have kept the default name as ‘Hari’ (When you ask, what is your name?). Based on the requests, many people wants to change it according to their experiment. So here is how you do it. Navigate to

app/src/main/assets/Hari/config/properties.txt

Under this file, change the ‘name’ parameter as you wish. To see the change, please uninstall the app, and delete the folder ‘hari’ inside your phone’s local storage(refer to the path in MainActivity.java). The concept is, each time you run the app, all the files will be copied to your local storage. If it is already present, it will not overwrite it. After deleting, run the app again to see the new name.

Limitations

This is just a proof of concept. Not production ready. It’s entirely up to you to build something out of it. The file size is bigger, as it has more files. Handling custom conversation is difficult as all files must to be modified manually.

The optimal way to build a bot is to use any Bot building frameworks and handling it everything online via API calls.

Storing chat history

Chat history can be stored in multiple ways. You can construct a local persistent storage and store all the inputs and responses or you can also maintain a file and write all the data to it. It’s entirely up to you.

I don’t have an answer to that (problem)

Well, this error can occur for different reasons. First please check whether a folder called ‘hari’ is created in your phone’s local storage, containing all the files from the project’s assets folder. If not, check whether the permissions are granted properly (for accessing local storage) and reinstall again. Only if the folder ‘hari’ is created, the bot can give you responses by reading from that files.

Reference Links