Suppose you are chatting with someone you can’t see, only by typed messages. They ask how you are, they pick up on what you say, they ask about your family. How sure are you that a person is typing the answers?
In 1966, at the Massachusetts Institute of Technology (MIT) in the United States, people sat at typewriters joined to a computer and typed to a program called ELIZA. It answered like a patient listener, and some of them became deeply involved with it and confided in it, as if it understood them. The program below is a small ELIZA of our own, built the same way. Talk to it about anything: school, exams, your family, the weather.
Before you look inside: how much does this ELIZA need to know, to answer like that?
Pick an answer to see why.
You need lists, dictionaries and functions in Python (Grades 8 and 9). Regular expressions come from Grade 11; the parts used here are explained as they come.
Try it
Talk to ELIZA a little more, with Show the rules on, and watch what each reply is made from.
- Type “I am tired of my phone”. The rule for “i am” found your words after it, tired of my phone, and turned them round to tired of your phone. You wrote half of ELIZA’s reply yourself.
- Type “I am a purple elephant”. ELIZA takes it as seriously as anything else. Then “I am not sad”: the reply puts “not sad” in the place where it would put “sad”.
- Type something with none of its key words, such as “The bus was late”. ELIZA gives a stock reply, “Please go on” or “I see”, the next of 4 in turn. A stock reply fits almost anything a person says.
- Type “Everyone ignores me, I am sad”. There are two key words, “everyone” and “i am”. Which one did it answer?
Weizenbaum chose that role on purpose. His script made ELIZA answer roughly as a certain kind of psychotherapist does, one who mostly asks you to say more. In his paper he explains that in such a conversation one of the two is free to “assume the pose of knowing almost nothing of the real world”. If you tell such a therapist “I went for a long boat ride” and they say “Tell me about boats”, you don’t decide they know nothing about boats: you think they have a reason for asking.
How it works: patterns and reflections
Every reply ELIZA makes comes from three steps.
- a.Find a pattern. Go through the rules in order, and for each, look for its pattern in the text. The first rule whose pattern is found answers.
- b.Turn the words round. If the pattern has a part in brackets, a group, take the words it matched and swap the words that point at a speaker: my becomes your, i becomes you, you becomes me. This swapping is called reflection, as in a mirror.
- c.Fill in a reply. Each rule has a few replies with a gap,
{0}, for the reflected words. Use the rule’s replies in turn, so the same line doesn’t come back twice in a row.
Weizenbaum wrote his patterns in a notation of his own. One example from his 1966 paper is the pattern (0 YOU 0 ME), where each 0 stands for “any number of words”, with the reply (WHAT MAKES YOU THINK I 3 YOU), where 3 means “the third part of what matched”. So “You hate me” becomes “WHAT MAKES YOU THINK I HATE YOU”. Today the same idea is written as a regular expression with a group: you (.*) me, then “What makes you think I {0} you?”.
The pattern for “i am”
ELIZA’s busiest rule looks for “i am” and keeps everything after it. Before writing it in Python, get the pattern right: it is harder than it looks. The lines have already been made lower case, as ELIZA does first, so “I” is “i”.
Which rule wins?
“Everyone ignores me, I am sad” has two key words. ELIZA answers the first rule in its list that matches, and the list puts “everyone” before “i am”. That order is on purpose. Weizenbaum gave key words a rank, and explained one in his paper: when people say “everybody” or “nobody”, they usually mean someone in particular, so “everybody” outranks “I”, and the reply asks who.
ELIZA’s rules are tried in this order: “everyone”, then “i am”, then “my (.*)”. What does it answer to “My brother says nobody likes me”?
Pick an answer to see why.
The other trick: cut at the comma
Weizenbaum’s ELIZA also cut what you typed into pieces at commas and periods, and answered only one piece. So “Well, my friend made me come here” loses its “Well” and becomes “YOUR FRIEND MADE YOU COME HERE”, nearly a line from the 1966 paper. Our ELIZA does the same: it tries its rules on the first piece, then the next, and answers the first piece where a rule is found.
Program it
You will build ELIZA in three steps, each running here on the page.
Step 1: one rule
re.search(pattern, text) looks for the pattern anywhere in the text, and gives back a match, or None. m.group(1) is what the first group matched.
import re text = "i am worried about my exams"m = re.search(r"\bi am (.*)", text)if m: print(m.group(1)) print("How long have you been " + m.group(1) + "?")Output
worried about my examsHow long have you been worried about my exams?Nearly. “My exams” is wrong: ELIZA is talking about your exams. The words have to be turned round.
Step 2: reflection
A dictionary says what each word becomes. reflect splits the words (Grade 9’s split), looks each one up, and keeps any word that isn’t in the dictionary as it is (get(word, word)).
import re REFLECT = {"i": "you", "me": "you", "my": "your", "mine": "yours", "myself": "yourself", "am": "are", "i'm": "you are", "i've": "you have", "you": "me", "your": "my", "yours": "mine", "yourself": "myself", "you're": "I am"} def reflect(words): out = [] for word in words.split(): out.append(REFLECT.get(word, word)) return " ".join(out) for text in ["i am worried about my exams", "i am angry with you", "i am sure my friend hates me"]: m = re.search(r"\bi am (.*)", text) print("How long have you been " + reflect(m.group(1)) + "?")Output
How long have you been worried about your exams?How long have you been angry with me?How long have you been sure your friend hates you?In the last line, “my friend hates me” has become “your friend hates you”: both words turned, and the sentence still makes sense. ELIZA knows no grammar at all, only this table, and it is right surprisingly often. Not always: give it “my sister says you are kind” and it makes “your sister says me are kind”, because the table can’t tell when you should become I and when me.
Step 3: the whole script
Now the rules go in a list: each one a pattern and its replies. respond makes the text lower case (and turns a phone’s curly ’ into a plain '), cuts it at punctuation with re.split, and tries every rule on each piece. The dictionary used counts how often each rule has answered, so that n % len(replies) takes its replies in turn. reply.format(...) fills {0} with the reflected group. This is the same script the chat at the top of the page runs.
import re REFLECT = {"i": "you", "me": "you", "my": "your", "mine": "yours", "myself": "yourself", "am": "are", "i'm": "you are", "i've": "you have", "you": "me", "your": "my", "yours": "mine", "yourself": "myself", "you're": "I am"} def reflect(words): out = [] for word in words.split(): out.append(REFLECT.get(word, word)) return " ".join(out) RULES = [ (r"\b(everyone|everybody|nobody|no one)\b", ["Who exactly do you mean?", "Is that true of every single person?"]), (r"\balways\b", ["Can you remember one particular time?", "Every time, without exception?"]), (r"\bi need (.*)", ["Why do you need {0}?", "What would change if you got {0}?"]), (r"\bi(?: am|'m) (.*)", ["How long have you been {0}?", "Why do you say you are {0}?", "How does being {0} make you feel?"]), (r"\bi feel (.*)", ["Tell me more about feeling {0}.", "When do you feel {0}?"]), (r"\bi can'?t (.*)", ["What makes you think you can't {0}?", "What would happen if you tried to {0}?"]), (r"\bmy (mother|father|mom|dad|sister|brother|family|aama|buwa|didi|dai)\b", ["Tell me more about your family.", "How do you get on with your {0}?"]), (r"\bmy (.*)", ["Your {0}?", "Why does your {0} matter to you?"]), (r"\byou are (.*)", ["What makes you think I am {0}?", "Would you like me to be {0}?"]), (r"\bbecause\b", ["Is that the only reason?", "What else might explain it?"]), (r"\b(exams?|tests?|marks|results)\b", ["What worries you most about your {0}?", "How do you usually feel before {0}?"]), (r"\b(computers?|machines?|robots?)\b", ["Do computers worry you?", "Why are you thinking about {0}?"]), (r"^(yes|yeah)\b", ["You sound sure.", "Go on."]), (r"^no\b", ["Why not?", "You sound very firm about that."]),]OTHERWISE = ["Please go on.", "Tell me more.", "I see.", "What does that suggest to you?"]used = {} def respond(text): text = text.lower().replace("’", "'") for phrase in re.split(r"[.,;!?]", text): for pattern, replies in RULES: m = re.search(pattern, phrase.strip()) if m: n = used.get(pattern, 0) used[pattern] = n + 1 reply = replies[n % len(replies)] return reply.format(*[reflect(g) for g in m.groups()]) n = used.get("", 0) used[""] = n + 1 return OTHERWISE[n % len(OTHERWISE)] for line in ["Well, my friend made me come here.", "I am worried about my exams.", "Everyone expects too much of me.", "My father says I never study.", "Because I stay up late.", "You are not very helpful."]: print("You: ", line) print("ELIZA:", respond(line))Output
You: Well, my friend made me come here.ELIZA: Your friend made you come here?You: I am worried about my exams.ELIZA: How long have you been worried about your exams?You: Everyone expects too much of me.ELIZA: Who exactly do you mean?You: My father says I never study.ELIZA: Tell me more about your family.You: Because I stay up late.ELIZA: Is that the only reason?You: You are not very helpful.ELIZA: What makes you think I am not very helpful?A few of the replies are Weizenbaum’s own, from his 1966 paper and the script printed at its end: “Please go on”, “I see”, “What does that suggest to you?”, “Tell me more about your family”, “Do computers worry you?” and “How long have you been …?”. The rest are ours. *[...] in the return line hands the list’s items to format one by one, so a rule with two groups fills {0} and {1}.
Now talk to your program. The chat below calls your respond (from the program above, as you have it: press Edit to change it) for every line you send.
What your program shows
Feed your ELIZA some lines no listener could take seriously, then ask it what it knows about you.
import re REFLECT = {"i": "you", "me": "you", "my": "your", "mine": "yours", "myself": "yourself", "am": "are", "i'm": "you are", "i've": "you have", "you": "me", "your": "my", "yours": "mine", "yourself": "myself", "you're": "I am"} def reflect(words): out = [] for word in words.split(): out.append(REFLECT.get(word, word)) return " ".join(out) RULES = [ (r"\b(everyone|everybody|nobody|no one)\b", ["Who exactly do you mean?", "Is that true of every single person?"]), (r"\bi(?: am|'m) (.*)", ["How long have you been {0}?", "Why do you say you are {0}?"]), (r"\bmy (.*)", ["Your {0}?", "Why does your {0} matter to you?"]),]OTHERWISE = ["Please go on.", "Tell me more.", "I see.", "What does that suggest to you?"]used = {} def respond(text): text = text.lower().replace("’", "'") for phrase in re.split(r"[.,;!?]", text): for pattern, replies in RULES: m = re.search(pattern, phrase.strip()) if m: n = used.get(pattern, 0) used[pattern] = n + 1 reply = replies[n % len(replies)] return reply.format(*[reflect(g) for g in m.groups()]) n = used.get("", 0) used[""] = n + 1 return OTHERWISE[n % len(OTHERWISE)] for line in ["I am a purple elephant.", "I am not sad.", "My phone fell in the river!", "Glorp zibble frum.", "Nobody understands me, but you do."]: print("You: ", line) print("ELIZA:", respond(line)) print()print("Everything it remembers about you:")for pattern in used: print(used[pattern], "x", pattern or "(no rule: a stock reply)")Output
You: I am a purple elephant.ELIZA: How long have you been a purple elephant?You: I am not sad.ELIZA: Why do you say you are not sad?You: My phone fell in the river!ELIZA: Your phone fell in the river?You: Glorp zibble frum.ELIZA: Please go on.You: Nobody understands me, but you do.ELIZA: Who exactly do you mean? Everything it remembers about you:2 x \bi(?: am|'m) (.*)1 x \bmy (.*)1 x (no rule: a stock reply)1 x \b(everyone|everybody|nobody|no one)\b(Only three of the rules are kept here, to keep the program short; with all 14 the replies are the same.) Every reply sounds attentive, and every one is made the same way: a pattern found, your words turned round. A purple elephant is answered as seriously as a worry, nonsense gets “Please go on”, and “Nobody understands me, but you do” gets a question about who. At the end of the conversation, all that your ELIZA keeps is four numbers: how often each rule answered. Not one word you typed.
So the understanding in an ELIZA conversation is all on one side: yours. You read meaning into its replies because people make sense of what they hear; that is what conversation is. The tendency to believe that a program understands you because its words sound human is now called the ELIZA effect, and Grade 10’s Human and machine intelligence shows how strong it is with today’s far more fluent chatbots.
Going further still
ELIZA’s memory
The 1966 conversation ends with the line people remember: after talking about their father, the user types “Bullies.”, and ELIZA answers “DOES THAT HAVE ANYTHING TO DO WITH THE FACT THAT YOUR BOYFRIEND MADE YOU COME HERE”, from the user’s third line, “Well, my boyfriend made me come here.” It looks like deep understanding. It is a queue: whenever “my” was the most important key word in a line, ELIZA also made a reply from that line and put it aside, first in, first out; when a later line had no key word, ELIZA could use the oldest reply put aside instead of “Please go on” (a counter decided when). Challenge ★★★ builds a simpler version.
ELIZA in Nepali
Weizenbaum’s scripts were data, so ELIZA could talk Welsh. Yours can talk Nepali: the rules are only patterns and replies.
An open question
Your ELIZA can’t pass Turing’s test for long: ask it anything about the world (“What is the capital of Nepal?”) and it says “Please go on.” Today’s chatbots answer that and much more. Does passing the test mean a program thinks, or only that it imitates someone who does? Turing thought the question “Can machines think?” too meaningless to discuss, and offered his game instead. More than seventy-five years later, people still argue about whether he was right to swap them.
Challenges
Three challenges, each checked on the page: write your function in the program above the checker (press Edit), then press Check my program. The examples show what your function should give; there are more tests you can’t see. Every text the tests give is already lower case.
★ A rule for wanting
Write want_reply(text): if the text has the words “i want” followed by something, give back “What would it mean to you if you got …?” with the something reflected; otherwise give back None.
import re REFLECT = {"i": "you", "me": "you", "my": "your", "mine": "yours", "myself": "yourself", "am": "are", "you": "me", "your": "my"} def reflect(words): return " ".join(REFLECT.get(w, w) for w in words.split()) def want_reply(text): # Give back the reply, or None if "i want ..." isn't there. return None print(want_reply("i want my phone back"))★★ The highest rank wins
Weizenbaum’s key words had ranks. Write best_reply(text, rules): rules is a list of [rank, pattern, reply]. Of the rules whose pattern is found in the text, the one with the highest rank answers (if two have the same rank, the one earlier in the list); give back its reply with {0}, {1} filled by its groups as they are (no reflecting). If no pattern is found, give back “Please go on.”
import re def best_reply(text, rules): # Give back the reply of the highest-ranked rule that matches. return "Please go on." RULES = [[0, r"\bi am (.*)", "How long have you been {0}?"], [5, r"\b(everyone|everybody|nobody)\b", "Who exactly do you mean?"], [2, r"\bmy (mother|father)\b", "Tell me more about your family."]] print(best_reply("i am sure everyone hates me", RULES))★★★ “Earlier you said…”
Write talk(lines): ELIZA’s reply to each line in turn, as a list. Use the three RULES in the program (the first that matches answers, its group reflected). Add ELIZA’s memory: whenever a line has my (.*) in it, also make the sentence “Does that have anything to do with the fact that your …?” from the reflected words, and put it at the end of a queue. When a line matches no rule, the reply is the sentence at the front of the queue (taken off it), or “Please go on.” if the queue is empty.
import re REFLECT = {"i": "you", "me": "you", "my": "your", "you": "me", "your": "my", "am": "are"} def reflect(words): return " ".join(REFLECT.get(w, w) for w in words.split()) RULES = [(r"\bi am (.*)", "How long have you been {0}?"), (r"\bmy (.*)", "Your {0}?"), (r"\byou are (.*)", "What makes you think I am {0}?")] def talk(lines): # Give back a list: ELIZA's reply to each line. return [] print(talk(["my boyfriend made me come here", "i am depressed", "bullies"]))Questions
What does ELIZA do with the words “i am” in “i am worried about my exams”?
Pick an answer to see why.
Why does ELIZA have stock replies like “Please go on” and “I see”?
Pick an answer to see why.
Explain what the ELIZA effect is, using what your program kept at the end of its conversation.
Answer
A friend adds the rule (r"\bi (.*)", ["Why do you say you {0}?"]) at the top of the list of rules. What goes wrong?
Answer
In Turing’s imitation game, why are the answers typed, not spoken?
Answer
ELIZA in 1966 and a chatbot today both produce sentences that sound human. Give one way they work differently, and one reason to be careful with both.
Answer
Where to go next
- Grade 10, How Large Language Models Work: what today’s chatbots really do, from tokens to training, and the arguments about whether they understand.
- Grade 11, Automate Your Work: regular expressions in full, for finding amounts, dates and phone numbers in real text.
- Grade 12, The Limits of Computing: every regular expression is a simple machine, and what such machines can never do.
- Grade 12, Technology, Policy and You: can a machine think, and who is responsible when it acts.
