Creating An Eliza IRC Bot: Using the Comprehensive Perl Archive Network

The Perl community is one of the most well-established demonstrations of the Open Source Software movement. Many people that have benefited from Perl's openness have in turn contributed libraries and scripts back to the public for others to use. The collective contributions to the Perl community have been organized into the Comprehensive Perl Archive Network, known more commonly as the CPAN.

The Perl community is one of the most well-established demonstrations of the
Open Source Software movement. Many people that have benefited from Perl’s
openness have in turn contributed libraries and scripts back to the public for
others to use. The collective contributions to the Perl community have been
organized into the Comprehensive Perl Archive Network, known more commonly as the

The CPAN is not a single machine, but actually over 100 machines holding more
than 750 MB of cool stuff, available for your access via anonymous FTP or, in a
few cases, HTTP. They all mirror off the master site in Finland at various
intervals, usually not exceeding four to eight hours, so it’s usually safe to use
the CPAN archive nearest to you. If you’re in the US, visit http://www.cpan.org.
If that site is down (which it occasionally is),or you’re somewhere else in the
world, try http://www.perl.com/CPAN/, which should send you to an up-and-running
site that is close to you.

Once you get to the CPAN archive, you’ll find all sorts of useful things,
including the latest release of Perl for UNIX and other operating systems. In
particular, you’ll find hundreds of pre-written and tested Perl modules to handle
many common tasks.

Let’s look at two of those modules and how they might be used together in an
interesting way. The Net::IRC module provides support
for Perl to handle the Internet Relay Chat protocol. With this module, you can
write an IRC bot — a Perl program which connects to an IRC server as if it were
an IRC client and interacts with that server programmatically.

The second is one of my favorite modules, Chatbot::Eliza, which implements the classic Eliza algorithm. The original Eliza program was
written by Joseph Weizenbaum and was described in the Communications of the ACM
in 1966: Eliza is a mock Rogerian psychotherapist. It prompts for user input and
uses a simple transformation algorithm to change user input into a follow-up
question. The program is designed to give the appearance of understanding.

This program is a faithful implementation of the program described by
Weizenbaum. It uses a simplified script language devised by Charles Hayden. The
content of the script is the same as Weizenbaum’s.

I wanted to see what would happen if the Net::IRC
module was hooked into Chatbot::Eliza to make an IRC bot
that acts like a psychotherapist. The process was easier than I thought, and a
good example of how to reuse existing code from the CPAN.

To install the two modules needed in this program, simply do the

$ perl -MCPAN -eshell
cpan> install Net::IRC Chatbot::Eliza
[cpan installation stuff omitted]
cpan> quit

That’s all there is to it! This invocation will fetch the module source
from the nearest CPAN archive and unpack, configure, make, test, and install them.
You have to be the same user as the one that installed Perl. If you’re not,
invoke perldoc CPAN to get further instructions. Note
that you don’t have to install anything manually; CPAN.pm (which should be built into your current Perl installation) does it all for

Take a look at the “eliza bot” in the sidebar.

Line 1 begins the program, specifying the path to your installed Perl binary
(for me, this is /usr/bin/perl)and enabling warnings
(this is a good idea during development, but should always be disabled in

Line 2 enables the three common compiler restrictions. These restrictions
disable the use of soft references (always a good idea), disable barewords being
treated like strings (this keeps random typos from being ignored), and require
all variables to be either explicit package variables or pre-declared with my to make them lexical. Because these restrictions make
sense for Perl programs longer than ten lines or so, I usually enable them

Line 3 increments the value of $|. If you do this at
the beginning of the program, the STDOUT filehandle
becomes unbuffered instead of buffered. Here, that’s a good thing, because we
want to see the result of each print operation as it’s
being generated.

Lines 5 and 6 pull in the two modules as described earlier. These modules
must be locatable in the directories specified in the compile-time @INC variable. If you need to add directories to @INC, see the documentation for the lib module (by invoking perldoc

Lines 8 and 9 define two configuration constants for this program, each with
a true/false value. The $IRC_debug value is passed to
the Net::IRC module to enable debugging. If false, Net::IRC stays relatively quiet. If it’s set to true, we get
a fairly detailed tracing of all the steps and callbacks. I set $IRC_
to 1 while I was developing this code, but it’s too noisy
during normal operation, so I’ve set it to 0 here. The second constant, $TRACE, selects whether summary messages of everything the
bot is doing gets sent to standard output. These are pretty small and can
probably remain enabled during actual use.

Line 11 defines a new Net::IRC object, assigns it to
$irc, and sets the debugging value for this object
according to the value of $IRC_ debug, as defined above.
This object represents the supervisor of all connections to various IRC servers
for this particular program.

Lines 12 through 15 create an individual IRC connection. In this case, it is
the sole connection for this bot, connecting as nickname eliza000 to the IRC server at the fictional
host name random.place.not. Of course, to be useful, you
will need to find an IRC server on the appropriate IRC network that supports the
use of bots. (Most IRC servers do not allow bots, but some do.) When the code has
returned from newconn, $conn
holds a Net::IRC::Connection object — our main
interface for sending and receiving IRC packets.

The primary means of scripting a conversation with Net::IRC is to
establish a series of event handlers. Each handler is associated with a
particular event (transformed into
a Net::IRC::Event object). When that event is seen
(because of something the IRC server has sent to our bot), the corresponding
handler is called, along with some parameters. For example, if you want to know
every time someone sends a public message to a channel the bot has joined, you’ll
set up an event for public.

Lines 16 through 44 set up the particular event handlers that we need for
this bot. I’m using a foreach loop here (spelled f-o-r but pronounced “foreach” !) to walk $_ through a list of arrayrefs. Each arrayref in turn
contains two items: either a single word or another arrayref, followed by a
coderef. The body of the foreachloop (in line 43) passes
these arrayrefs as parameters to the add_global_handler
method on the connection, resulting in the installation of a particular coderef
as an event handler for one or more named events.

So, let’s look at each of the event handlers. Each one of these subroutines
will be called with two parameters: the connection object itself and the event

The first one, in lines 16 through 18,handles the motd event, triggered when we get a single line of text as part of the IRC sign-on
message. To get that line of text, we use the second element of the subroutine
arguments ($_[1]), which is the event object. Calling
the args method on that object yields a list, the second
element of which is the line of text we want. Finally, we dump that line out if
we’re tracing.

After the entire motd message from the server is
complete, we’ll get an endofmotd event, which is about
thefirst time that we can start issuing commands and messages. So, I register a
handler for endofmotd in lines 19 through 23.

Line 20 grabs the connection object into a local $conn. This is likely
to be the same value as the global $conn, but I’m
following safe protocol here by taking it from the argument list. Line 21 prints
out a trace
to let me know I’m all the way in.
Line 22 invokes the equivalent of the
IRC command join(“#doctors_ office”), letting us start
receiving public messages for that channel. If the join failed, a later event
will tell me (which I’m ignoring), so there’s no error checking to be done right

Lines 24 through 31 handle a nickname collision. The heart of this handler is
line 28, which uses Perl’s
magical autoincrement to change eliza000 into eliza001 and then eliza002 and so
on, until we get one that isn’t taken. This works only if the attempted nickname
is of a form that Perl can autoincrement. This was verified in line 27.

Lines 32 through 36 handle all private messages, such as when someone says
/msgeliza000 hi. In this handler, we’ll extract the
nickname from which the message came and the message itself, and pass those along
with the original connection and event objects to a subroutine heard, deined below.

Similarly, lines 37 through 40 handle all public messages, when someone sends
a message in a channel that this bot has joined. Here, I’ll call the same heard subroutine, but pass along the channel name as the
source, rather than the nickname.

Line 45 is the top level loop. This method invocation only returns when the
IRC connection has been broken (usually after a user gets booted off the server,
or issues a quit command).

So, once we’ve started, everything else has to be handled within the context
of the various event handlers. Since we established those above, they’ll now be
called as things come in.

Lines 47 through 74 define the heard subroutine.
This subroutine has two local variables that persist throughout the life of the
program, defined in lines 48 and 49. %docs is the hash
mapping a particular username to a Chatbot::Eliza object
(a “doc”). And %talking_to keeps track of the nickname
to which messages are being sent so that a fairly natural-looking nickname prefix
can be added when the doctor changes the way she is “facing” in the channel.

Line 52 extracts the four parameters being handed to the heard subroutine: the connection object, the event object, a
nick or channel name from which the message came, and the message itself. Line 53
dumps that out if we’re tracing.

Lines 54 through 57 establish a magic phrase to make the doctor go away nicely. If any private or public message contains “go
away”, then we’ll request a quit (with a cute line from Elton John’s
“Rocketman”), and return without any further processing.

Otherwise, it’s time to hand the line being spoken to a doctor. The doctor
memory of prior spoken lines needs to be maintained on a per-user basis, and the
easiest way to do this is to take the user information in line 58 and look it up
in a hash of doctors (%docs) in line 59 to get a
specific doctor into $doc. Now, if this is the first
time the bot has seen a particular user, we’ll need to create a doctor object in
lines 60 through 62. We will invoke the new method in Chatbot::Eliza to establish a new

Now comes the slightly embarrassing part. While I was playing with this
program, I found that the version of Eliza on the CPAN for the past six months
(version 0.40) has had a bug in it that causes the memories for various bots to
all be shared. This has caused some weird responses — someone would ask about
dogs, and another person would be told “Earlier, you said something about dogs”,
ruining some of the illusion. Until this is fixed, we’ll work around it with line
61. Pay no attention to the man behind the curtain here…. If you’re using a
version greater than 0.40, most likely you won’t need to do this.

Once we have a doctor (either new or returning), we’ll tell her what this
user said in line 64, and get her reply. Line 65 grabs the nickname of the user
that originally spoke the line and lines 66 through 69 figure out if we’re
already talking to that person. If not, line 68 prefixes the first line of the
response with the nickname followed
by a comma. Subsequent messages will no longer be prefixed, because the nickname
in $nick will match the value of $talking_to{$from}, the most recently spoken-to nickname in a
particular channel.

Finally, lines 70 through 72 speak all the response lines in sequence, using
the subroutine say defined later.

Lines 76 through 84 handle the response. Initially, privmsg responses were used for everything, but this violated
the agreement that bots are neverto use privmsgs to
people, because itcan trigger a return response that could get into an
auto-response meltdown. (The exact rule is that bots are never to listen to notice messages, and are never to generate anything except
notice messages, but the rules are somewhat relaxed for
public channel messages.) So, we’ll use privmsgs for
channels and notices for users.

There you have it — a simple demonstration of two neat modules from the
CPAN, put together to provide hours of enjoyment. One beta tester just spent an
hour and a half talking to the doctor while I was writing this. I guess some
people don’t have anything better to do.

“Eliza IRC Bot” Perl

1 #!/usr/bin/perl -w
2 use strict;
3 $|++;

5 use Net::IRC;
6 use Chatbot::Eliza;

8 my $IRC_debug = 0;
9 my $TRACE = 1;

11 (my $irc = Net::IRC->new)->debug($IRC_debug);
12 my $conn = $irc->newconn(
13 Nick => ‘eliza000′,
14 Server => ‘random.place.not’,
15 );
16 for ([motd => sub {
17 print"motd:".($_[1]->args)[1],”\n” if $TRACE;
18 }],
19 [endofmotd => sub {
20 my $conn = shift;
21 print "we are IN!\n" if $TRACE;
22 $conn->join("#doctors_office");
23 }],
24 [nicknameinuse => sub {
25 my $conn = shift;
26 my $nick = $conn->nick;
27 $nick =~ /^[a-zA-Z]+[0-9]*$/ or die “can’t
fix collided nick”;
28 $nick++;
29 print”nickcollision,fixingto$nick\n”if $TRACE;
30 $conn->nick($nick);
31 }],
32 [msg => sub {
33 my ($conn, $event) = @_;
34 my ($msg) = $event->args;
35 heard($conn, $event, $event-> nick, $msg);
36 }],
37 [public => sub {
38 my ($conn, $event) = @_;
39 my ($msg) = $event->args;
40 heard($conn, $event, $event->to, $msg);
41 }],
42 ){
43 $conn->add_global_handler(@$_);
44 }
45 $irc->start;
47 BEGIN {
48 my %docs;
49 my %talking_to;

51 sub heard {
52 my ($conn,$event,$from,$said) = @_;
53 print “heard $from say $said\n” if $TRACE;
54 if ($said =~ /go away/) {
55 $conn->quit(“o/~ and all the science, I don’t
understand… it’s just my job
five days a week o/~”);
56 return;
57 }
58 my $userhost = $event->userhost;
59 my $doc = $docs{$userhost} ||= do {
60 my $bot = Chatbot::Eliza->new();
61 $bot->{memory} = []; # bug workaround
62 $bot;
63 };
64 my @response = $doc->transform ($said);
65 my $nick = $event->nick;
66 if (($talking_to{$from} || “”) ne $nick) {
67 $talking_to{$from} = $nick;
68 $response[0] = “$nick, $response[0]“;
69 }
70 for (@response) {
71 say($conn, $from, $_);
72 }
73 }
74 }

76 sub say {
77 my ($conn, $to, $what) = @_;
78 print “telling $to $what\n” if $TRACE;
79 if ($to =~ /^\#/) { #a channel
80 $conn->privmsg($to, $what);
81 } else { #a person
82 $conn->notice($to, $what);
83 }
84 }

Randal L. Schwartz is the chief Perl guru at Stonehenge Consulting and
Learning Perl and Programming Perl. He can be reached at merlyn@stonehenge.com.

Comments are closed.