Skip to content

← Computing Further 9–12

Can a Computer Fool You into Thinking It’s a Person?

Topic 28 · Grades 9–12 · Chatbots

You need lists, dictionaries and functions in Python (Grades 8 and 9). The regular expressions it uses are explained here; Grade 11 has the whole story.

What this builds on from class
In this unit 8 sections
  1. Try it
  2. How it works: patterns and reflections
  3. Program it
  4. What your program shows
  5. Going further still
  6. Challenges
  7. Questions
  8. Where to go next

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.

Try it

Talk to ELIZA

Type a line and press Send (or tap one of the lines under the box). Say a few things, then press Show the rules.

Hello. Tell me what is on your mind.

Rules in its script
14
Your lines
0
Stock replies
0

Type anything and press Send, or tap one of the lines under the box. Then press Show the rules.

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”.

Try it

Find “i am” and “i’m”, and nothing else

The pattern starts as i am. It finds some lines it shouldn’t (“hi amma” has the letters “i am” in it), and misses “i’m”. Change the pattern until every line is right.

  1. ✓ i am tired should match
  2. ✓ today i am happy should match
  3. ✗ i'm worried about my exams should match
  4. ✗ well, i'm not sure should match
  5. ✗ hi amma, i ate already shouldn’t match
  6. ✗ the taxi among the buses shouldn’t match
  7. ✗ i amaze myself shouldn’t match
  8. ✓ my sister is tired shouldn’t match
Lines right
3 of 8

Line 3 should match, but doesn’t.

Hint
Answer

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.

Try it

Talk to your own ELIZA

Your respond gets each line you send and gives back ELIZA’s reply. Running the program first also printed its six-line conversation, so some replies are already part-way through their turns.

Type a line and press Send: the page calls your respond with it and shows what it gives back.

What your respond prints, and any error, appears here.

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"))

Try it

★ A rule for wanting

  1. My phone back want_reply('i want my phone back') Should give 'What would it mean to you if you got your phone back?' Yours —
  2. Nobody wants it want_reply('nobody wants me') Should give None Yours —
Tests passed
— / 8
Challenge
★ solved

Write your want_reply in the program above (Edit), then check it.

A test your function gets wrong, anything it prints, and any error appear here.

Hint
Answer

★★ 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))

Try it

★★ The highest rank wins

  1. “everyone” outranks “i am” best_reply('i am sure everyone hates me', [[0, '\\bi am (.*)', 'How long have you been {0}?'], [5, '\\b(everyone|everybody|nobody)\\b', 'Who exactly do you mean?'], [2, '\\bmy (mother|father)\\b', 'Tell me more about your family.']]) Should give 'Who exactly do you mean?' Yours —
  2. Only “i am” best_reply('i am tired', [[0, '\\bi am (.*)', 'How long have you been {0}?'], [5, '\\b(everyone|everybody|nobody)\\b', 'Who exactly do you mean?'], [2, '\\bmy (mother|father)\\b', 'Tell me more about your family.']]) Should give 'How long have you been tired?' Yours —
Tests passed
— / 8
Challenge
★★ solved

Write your best_reply in the program above (Edit), then check it.

A test your function gets wrong, anything it prints, and any error appear here.

Hint
Another hint
Answer

★★★ “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"]))

Try it

★★★ “Earlier you said…”

  1. The 1966 ending talk(['my boyfriend made me come here', 'i am depressed', 'bullies']) Should give ['Your boyfriend made you come here?', 'How long have you been depressed?', 'Does that have anything to do with the fact that your boyfriend made you come here?'] Yours —
Tests passed
— / 6
Challenge
★★★ solved

Write your talk in the program above (Edit), then check it.

A test your function gets wrong, anything it prints, and any error appear here.

Hint
Another hint
Answer

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.

a.

Explain what the ELIZA effect is, using what your program kept at the end of its conversation.

Answer
b.

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
c.

In Turing’s imitation game, why are the answers typed, not spoken?

Answer
d.

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