Python for AI: A Crash Course

Nov 16 2024 · Python 3.12, JupyterLab 4.2.4

Lesson 01: Introduction to Python & Jupyter Notebooks

ELIZA Demo

Episode complete

Play next episode

Next
Transcript

Demo

In this demo, you’ll write the main code for a version of ELIZA, the original chatbot and distant ancestor of today’s LLMs. ELIZA simulated a session with a Rogerian psychotherapist, with the user in the role of the patient and the application encouraging the user to explore their thoughts and feelings through conversation.

Originally written by computer scientist Joseph Weizenbaum at MIT’s Artificial Intelligence Lab in the mid-1960s for the room-sized IBM 7094 computer, it enjoyed a revival in the 1980s on home computers like the Apple II and Radio Shack TRS-80.

ELIZA looks for patterns in the user’s input and matches them with pre-written responses. For example, if the user entered “I feel uncertain,” it would choose from a list of “I feel…” responses, such as “When you feel uncertain, what do you do?” ELIZA’s responses were designed to encourage the user to keep the conversation going.

Although ELIZA seems primitive in the post-ChatGPT era, many people who used it in the 1960s found it quite convincing and were convinced that they were talking to a human. In a particularly personal session, Weizenbaum’s secretary, one of his testers, asked him to leave the room.

This demo covers a lot of the material from the lesson — variables, loops, branching, lists, dictionaries, regular expressions, and more. To save you the agony of a lot of typing, the starter project contains a couple of predefined dictionaries containing ELIZA’s canned responses so that you can concentrate on Python coding and not on data entry.

Open Jupyter Lab in lesson 1 and open Eliza-starter.ipynb.

Add the import Statements

Scroll down to the Markdown cell with the heading Imports.

In the empty code cell below the Markdown cell, enter the following code and run it:

from random import choice
import re

This code will import:

  • The choice() method from the random module. To seem more “real,” ELIZA has a variety of responses for each prompt it recognizes and randomly selects one.
  • The re (regular expressions) module, which ELIZA uses to match key phrases from the user’s input.

Explore ELIZA’s Possible Responses

Scroll to the Markdown cell with the heading The responses Dictionary.

Below that cell is a code cell that defines a large dictionary named responses, where:

  • Each key is a string that defines one of the kinds of user input that ELIZA will respond to. For example, if the user entered “I need a vacation,” that input would get categorized as having the pattern “I need _,” where the underscore represents the text after “I need.” The pattern is used to look up possible responses. The keys are alphabetically ordered to make it easy for the programmer to change the responses.
  • Each value is a list of strings containing possible responses to that user input. ELIZA will choose one of these responses at random.

Run the responses cell.

In the cell below responses, try to access responses values using different keys. For example, enter the following and run it:

responses["i need _"]

The result will be a list of ELIZA’s possible responses to user input, beginning with “I need.” Some responses contain an underscore, which will be replaced by whatever text came after “I need.”

In the next cell, try the choice() function, which you imported from random. Given a list, choice returns a random item from that list. Run the following in a new code cell:

choice(responses["i need _"])

Try running that line repeatedly and notice that the responses change. Experiment with different keys.

Build ELIZA’s Dictionary of Prompt Patterns

Scroll down to the Markdown cell with the heading Build ELIZA’s Dictionary of Prompt Patterns.

Below that cell is a code cell that defines a large dictionary named prompts.

Run the prompts cell.

In the code cell below the prompts cell, run the following:

for key, value in prompts.items():
  print(f"{key}: {value}")

This will display the contents of prompts: each key and its corresponding value.

prompts defines a large dictionary where:

  • Each key is an r-string, a string where backslash characters are treated as literal backslash characters, not the start of an escape sequence. These strings will define a collection of regular expressions, which will be matched against the user’s input. The keys are in priority order; ELIZA will try to match the user’s input against the keys’ patterns in order.
  • Each value is a string used as a key to look up responses in the responses dictionary.

The dictionary is divided into sections to deal with different kinds of user input:

  • The enemy!: If the user input contains the word “ChatGPT,” ELIZA will express its disgust.
  • First-person prompts: If the user didn’t include “ChatGPT” in their input, ELIZA will then try to see if the user’s input is the kind where the subject is “I.”
  • Second and third-person prompts: If ELIZA doesn’t find any matches for first-person input, it tries to see if the user’s input is the kind where the subject is “you” or “it.”
  • If ELIZA doesn’t find any matches for second or third-person input, it tries to see if the user’s input has certain keywords mostly related to family.
  • Failing that, ELIZA tries to see if the user entered “yes,” “no,” or “maybe.”
  • And finally, there are the defaults — did the user end the input with a question mark or not?

ELIZA doesn’t directly use the prompts dictionary. Instead, it uses the prompts dictionary to build a dictionary called prompt_patterns where:

  • Each key will be a regular expression matched against the user’s input. ELIZA will iterate through the keys, looking for a regular expression that matches the user’s input.
  • Each value will be a string used as a key to look up responses in the responses dictionary. When ELIZA finds a key whose regular expression matches the user input, it uses the corresponding value to look up the possible responses.

Note: It may seem odd to use a regular expression as a dictionary key, but any object that is both immutable and hashable can be used as a key!

Enter the following in the same cell as prompts, immediately after prompts:

def pattern_to_regex(pattern):
  return re.compile(pattern, re.IGNORECASE|re.UNICODE)

def build_regex_list(patterns):
  result = {}
  for pattern in patterns:
    result[pattern_to_regex(pattern)] = patterns[pattern]
  return result

prompt_patterns = build_regex_list(prompts)

Here’s what the code does:

  • The pattern_to_regex() function takes a string containing a regular expression pattern and uses it to create a regular expression object using the re.compile() method. The extra argument, re.IGNORECASE|re.UNICODE tells re.compile() to make the regular expression object so that it ignores case and can match Unicode characters.
  • The build_regex_list() takes a dictionary whose keys are strings containing regular expression patterns and returns a similar dictionary, where the values are the same, but the keys are regular expression objects.
  • The final line defines the prompt_patterns dictionary, which ELIZA will use to determine how to respond to the user.

To see the resulting dictionary, run the following in the code cell after the prompts display code:

for key, value in prompt_patterns.items():
  print(f"{key}: {value}")

Build a Reflector

Scroll down to the Markdown cell with the heading Build a Reflector.

When ELIZA responds to the user, it needs to turn the user’s first-person words, “I,” “me,” “myself,” and so on, into second-person words, “you,” “yourself”, and vice versa.

In the code cell below the Markdown cell, enter the following code and run it:

word_reflections = {
  "am"   : "are",
  "was"  : "were",
  "i"    : "you",
  "i'd"  : "you would",
  "i'm"  : "you are",
  "i've"  : "you have",
  "we've" : "you have",
  "i'll"  : "you will",
  "mine" : "yours",
  "my"  : "your",
  "myself" : "yourself",
  "are"  : "am",
  "you've": "I have",
  "you'll": "I will",
  "your"  : "my",
  "you're" : "I am",
  "yours"  : "mine",
  "you"  : "me",
  "me"  : "you"
}

def reflect(original_text):
  original_words = original_text.lower().split()
  updated_words = []
  for word in original_words:
    if word in word_reflections:
      updated_words.append(word_reflections[word])
    else:
      updated_words.append(word)
  return ' '.join(updated_words)

Test the above code by running the following in the next code cell:

reflect("I want you to understand me")

You’ll get the result: “You want me to understand you.” Try using other strings with first and second-person words, keeping in mind that this is a primitive search-and-replace that doesn’t understand the rules of grammar.

Build ELIZA

You now have everything you need to build ELIZA.

Scroll down to the Markdown cell with the heading And Now, ELIZA!.

In the code cell below the Markdown cell, enter the following and run it:

def respond_to(patient_text):
  # Iterate through prompt_patterns, looking for
  # a regular expression that matches what the user entered
  for prompt_pattern in prompt_patterns:
    match = prompt_pattern.match(patient_text)

    # Found a match! Create a response
    if match:
      possible_responses = responses[prompt_patterns[prompt_pattern]]
      response = choice(possible_responses)

      # Fill in any blanks
      if response.find('_') > -1:
        phrase_to_reflect = match.group(1)

        # Remove any trailing punctuation
        if phrase_to_reflect[-1] in ".!?":
          phrase_to_reflect = phrase_to_reflect[:-1]

          reflected_phrase = reflect(phrase_to_reflect)
          response = response.replace('_', reflected_phrase)

        return response

def main():
  print(
    "Welcome to ELIZA!\n"
    "The original version was written by Joseph Weizenbaum in 1966.\n"
    "ELIZA simulates a Rogerian therapist, but is a bit touchy\n"
    "if you mention ChatGPT. You have been warned.\n"
  )
  while True:
    response = respond_to(input().strip())
    print(response)

The respond_to() function takes a string, which will be the user’s input, and then iterates through prompt_patterns keys, which are regular expression objects, looking for one that matches the user input. When it finds a matching key, it uses the corresponding value as a key for the prompts dictionary to retrieve the corresponding value, which is a list of possible responses. It then randomly chooses one of the responses.

If the response contains an underscore character, it determines what should replace that character. For example, if the user entered “I need a vacation,” one of the possible responses is “Why do you need _?”. The function uses the regular expression object’s group() method to capture the text that follows “I need” — “a vacation” — and uses it to replace the underscore.

You can now run ELIZA! Enter main() in the next code cell, run the cell, and enjoy chatting with an artificial intelligence, 1960s style!

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction Next: Conclusion