250 lines
8.8 KiB
Markdown
250 lines
8.8 KiB
Markdown
+++
|
|
title = "Using Matrix to make Chatbot software from the 1960s available in 2018"
|
|
aliases = ["/docs/guides/using-matrix-to-make-chatbot-software-from-the-1960-s-available-in-2018"]
|
|
+++
|
|
|
|
In this article, we'll use Matrix, a Raspberry Pi and JavaScript to bring back
|
|
chatbot software from the 1960s: ELIZA.
|
|
|
|
## What is 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](https://en.wikipedia.org/wiki/MAD_(programming_language)) was a popular
|
|
programming system for IBM mainframes of the time, and
|
|
[SLIP](https://en.wikipedia.org/wiki/SLIP_(programming_language)) 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.
|
|
|
|
## 2005, online JavaScript version appeared
|
|
|
|
In 2005, [Norbert Landsteiner](https://twitter.com/mass_werk) 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.
|
|
|
|
## Making the bot
|
|
|
|
### Using the library
|
|
|
|
For this project I have chosen to use an adaptation of the elizabot.js library:
|
|
[eliza-as-promised](https://github.com/natelewis/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:
|
|
|
|
```javascript
|
|
const Eliza = require('eliza-as-promised');
|
|
var eliza = new Eliza();
|
|
```
|
|
|
|
We can get an initial greeting as follows:
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
eliza.getResponse("Help me Eliza!")
|
|
.then((response) => {
|
|
if (response.reply) {
|
|
console.log(response.reply);
|
|
}
|
|
if (response.final) {
|
|
console.log(response.final);
|
|
process.exit(0);
|
|
}
|
|
});
|
|
```
|
|
|
|
### Using Matrix to make a modern Chatbot
|
|
|
|
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.
|
|
|
|
![Matrix Design](/docs/legacy/matrix-diagram.png)
|
|
|
|
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].
|
|
|
|
### Code Walkthrough
|
|
|
|
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](https://t2bot.io/docs/access_tokens/).
|
|
|
|
```javascript
|
|
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`](https://github.com/natelewis/eliza-as-promised) to create
|
|
new instances of Eliza, let's start by doing so whenever the bot joins a new
|
|
room.
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```javascript
|
|
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.
|
|
|
|
1. When an event is received in a room, we react to the event with a function
|
|
that takes the `roomId` and `event` received as parameters.
|
|
2. If the user is our own, or the event does not contain a body, we must
|
|
return.
|
|
3. We use the `roomId` to access the specific `Eliza` instance, as previously
|
|
created.
|
|
4. As above, we call `getResponse()` on the Eliza object, and we pass the
|
|
message string we just received.
|
|
5. We handle the promise returned by `getResponse()` by extracting the response
|
|
string into `responseText`, and use that string as a message to send back
|
|
into the room as a response.
|
|
6. Finally, if the response text we took from our Eliza is "final", we have
|
|
Eliza leave the room. (The bot can always be re-invited!)
|
|
|
|
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`.
|
|
|
|
## Deploying on a Raspberry Pi
|
|
|
|
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](https://www.raspberrypi.org/documentation/usage/terminal/) (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.
|
|
|
|
![Physical Pi](/docs/legacy/elizabot-pi.png)
|
|
|
|
To learn more about Matrix development, take a look at the [Matrix Guides
|
|
](https://matrix.org/docs/guides) page, and join us in the [
|
|
#matrix-dev:matrix.org](https://matrix.to/#/#matrix-dev:matrix.org) room!
|
|
|
|
[matrix-bot-sdk]: https://github.com/turt2live/matrix-bot-sdk
|
|
[TravisR]: https://github.com/turt2live
|