In this article, we'll use Matrix, a Raspberry Pi and JavaScript to bring back chatbot software from the 1960s: ELIZA.
ELIZA is a computer program written between 1964 and 1966, which imitates the conversation style of a psychotherapist of the era. It behaves like what we would now call a chatbot, but there was no such concept at the time - in fact there were only the first glimpses of text chat as we would recognise it today.
The original program was designed and at the MIT Artificial Intelligence Laboratory by Joseph Weizenbaum. ELIZA was first implemented on the IBM 7094 and written in MAD-SLIP, where MAD was a popular programming system for IBM mainframes of the time, and SLIP was a list-processing extension of Weizenbaum's own creation.
Seeing the results, in which users were highly willing to interact with ELIZA, Weizenbaum was disturbed by just how effective the apparently simple bot was. He was motivated to write a book explaining the way people tend to anthropomorphise technology by applying their own experiences to their usage.
In 2005, Norbert Landsteiner created a JavaScript program which he made available as elizabot.js, and made ELIZA available online through a web interface: https://www.masswerk.at/elizabot/.
Now we'll make this available on Matrix.
For this project I have chosen to use an adaptation of the elizabot.js library: eliza-as-promised, which exposes the bot as a promise-based node.js module. This module makes working with ELIZA very simple. First we create an instance of the bot object:
const Eliza = require('eliza-as-promised');
var eliza = new Eliza();
We can get an initial greeting as follows:
console.log(eliza.getInitial());
The getResponse()
function takes a string from the user, and returns a
Promise
with a response, with either a reply
or final
field. If the
response is a reply, we can give another string to get a response, if the
response is final, the session is over. For example:
eliza.getResponse("Help me Eliza!")
.then((response) => {
if (response.reply) {
console.log(response.reply);
}
if (response.final) {
console.log(response.final);
process.exit(0);
}
});
Matrix is an open standard for real-time communication over IP. It is often used to enable Instant Messaging. Matrix is not just Open Source, it is also designed to be interoperable, which makes it easy to take existing projects and integrate them.
Matrix provides a decentralised architecture, in which servers connect to one another, but as a user your client connects to a single homeserver, as described in the diagram below.
However, for this project we only need to look at the Client-Server API, which is the way in which clients and servers communicate with one another. To make it easier to connect the ELIZA library to Matrix, I chose to use a JavaScript library designed to access the Matrix Client-Server API: matrix-bot-sdk from TravisR.
First, we'll need a new instance of the Bot SDK, which we can obtain from NPM as follows:
npm install matrix-bot-sdk
Then, in our application code, we will instantiate and start a new client. Note that you can obtain an access token for the bot using Element.
var access_token = "...";
const MatrixClient = require("matrix-bot-sdk").MatrixClient;
const AutojoinRoomsMixin = require("matrix-bot-sdk").AutojoinRoomsMixin;
const client = new MatrixClient("https://matrix.org", access_token);
AutojoinRoomsMixin.setupOnClient(client);
client.start().then(() => console.log("Client started!"));
I also added the AutojoinRoomsMixin at this point, which instructs the bot to accept any room invitation it receives.
Knowing that we can use
eliza-as-promised
to create
new instances of Eliza, let's start by doing so whenever the bot joins a new
room.
var elizas = {};
client.on("room.join", (roomId) => {
elizas[roomId] = {
eliza: new Eliza(),
last: (new Date()).getTime()
}
client.sendMessage(roomId, {
"msgtype": "m.notice",
"body": elizas[roomId].eliza.getInitial()
});
});
What's happening here? First we make a new object to contain a mapping of room
IDs to Eliza
objects. When we get a room join event, we assign a new Eliza
object and the current time to the room ID. Next, we use the newly created
object to send the initial greeting to the room, using the same call to
getInitial()
we used earlier.
Next, we'll accept messages from the user and provide a response:
client.on("room.message", async function (roomId, event) {
if (event["sender"] === await client.getUserId()) return;
if (! event.content || ! event.content.body) return;
elizas[roomId].eliza.getResponse(event.content.body)
.then((response) => {
var responseText = '';
if (response.reply) { responseText = response.reply; }
if (response.final) { responseText = response.final; }
client.sendMessage(roomId, {
"msgtype": "m.notice",
"body": responseText,
"responds": {
"sender": event.sender,
"message": event.content.body
}
}).then((eventId) => {
if (response.final) {
client.leaveRoom(roomId);
}
});
});
});
It looks like a lot of code, but in fact we can break down what is happening here quite simply.
roomId
and event
received as parameters.roomId
to access the specific Eliza
instance, as previously
created.getResponse()
on the Eliza object, and we pass the
message string we just received.getResponse()
by extracting the response
string into responseText
, and use that string as a message to send back
into the room as a response.That all the code needed to get a working version of the bot running. If you look at https://github.com/benparsons/elizabot, you can find a simple implementation as described here in simple.js
, and a more robust implementation in index.js
.
To run the bot, we simply run node index.js
. However, part of this project is
to get the bot running on a Raspberry Pi - and for convenience the way to do
this is to have it run as soon as the device boots. Luckily, there are standard
ways of achieving this, one of which is to use systemd
. Open a shell on your
Pi (possibly using a
graphical desktop if one is installed, or using ssh) and enter this command to
access the correct directory:
cd /etc/systemd/system
Now, we'll use vim to create a file describing the service we want to create:
vim elizabot.service
Finally, enter the following text (you may need to change paths depending on where your script is located):
[Unit]
Description=Elizabot
After=multi-user.target
[Service]
Type=idle
ExecStart=/usr/bin/node /home/pi/elizabot/index.js > /home/pi/elizabot/log.log 2>&1
[Install]
WantedBy=multi-user.target
... and run the following to enable the service:
sudo systemctl enable elizabot.service
Now, whenever you plug in your Pi, your bot will be launched and ready to go. Of course, it is quite possible to run this software on a server, but having a separate box makes it more fun. The physicality of a Raspberry Pi means it gets more attention and understanding from people who see it.
To learn more about Matrix development, take a look at the Matrix Guides page, and join us in the #matrix-dev:matrix.org room!