11
\$\begingroup\$

I've thrown together this simple chat program, and while it works. I think it could certainly use some improvement. Here's how it works.

  1. Obtain user input by removing all punctuation from the input string, splitting it on whitespace, and converting it into a set.
  2. Loop through a list of possible input sets, and check the following condition: \$\text{Possible Input Set}\subseteq\text{User Input Set}\$
  3. Return a random reply based on the above condition, and output that reply.

Can this be made more flexible and extensible?

from re import sub as rsub
from random import choice


def obtain_user_input(prompt: str) -> set:
    """
    Obtains user input. This function 
    removes all punctuation, lowers text, 
    splits on whitespace, and returns a
    set of all the unique elements. An
    example input value could be:

        "Hello, hello, there, there"

    And the return value would be:

        ["hello", "there"]
    """
    user_input = input(prompt)
    user_input = rsub("[^a-zA-Z0-9\s]*", user_input, "")
    user_input = user_input.lower()
    user_input = user_input.split(" ")
    return set(user_input)


def get_replies(user_input: set) -> str:
    """
    This function returns a string as
    a "reply" to user input. This is done
    by checking if any of the possible
    inputs are subsets of the user's input.
    If so, then a random "reply" is returned.
    """
    possible_replies = [
        [{"hi"}, ["Hello there.", "Hello.", "Hi."]],
        [{"hello"}, ["Hello there.", "Hello.", "Hi."]],
        [{"goodbye"}, ["Goodbye user.", "Goodbye", "Bye."]],
        [{"your", "name"}, ["My name is not known.", "I cannot tell you my name,"]],
        [{"idiot"}, ["And so are you.", "Likewise."]],
        [{"you", "are", "smart"}, ["Why thank you!", "Thanks!"]]
    ]

    for possible_reply in possible_replies:
        if possible_reply[0].issubset(user_input):
            return choice(possible_reply[1])
    return "Say again?"


def main():
    while True:
        user_input = obtain_user_input("> ")
        reply = get_replies(user_input)
        print(reply)


if __name__ == "__main__":
    main()
\$\endgroup\$
  • 2
    \$\begingroup\$ I would suggest implementing exit into the goodbye, maybe? \$\endgroup\$ – Quill Jul 13 '15 at 23:51
10
\$\begingroup\$

Your python coding is great, there isn't anything I can come up with that affects your style.

I like the basic idea of your simple AI system, but it needs to be refined a bit to make it good in practice. In addition you can extend it with a lot of funky things.

The issues

First, let's take a look at the subset selection part. Here I can see 3 potential problems.

  1. What happens when someone posts a sentence containing words of multiple subsets? E.g. Hi, what is your name? -> contains Hi, your and name. I believe the answer will be a single Hi. or Hello there in return.
  2. Some groups seem to overlap: Hi and Hello seem like a same subset group to me.
  3. Words like are and you trigger answers like: thanks, which can be totally unrelated to the question.

How to fix them?

Natural language processing is a very hard task with multiple (partial) solutions going from easy approaches to very hard ones. Since you want to build a simple AI system, I would advise the following changes:

  • Get rid of words that are not important to the key message in the reply of the AI system. Words like are, you,... These will trigger to many false positives. Which solves issue 3.
  • Group similar words that should end up with the same answer-set together e.g. ('Hi','Hello'). I believe that was your original idea at the beginning, but you need to use it ;). This can solve issue 2.
  • Solving issue 1. has many possible approaches, I believe the simplest would be to check the input string for matches with multiple subsets. Keep some kind of hierarchy or ranking order of which subset should come first. Put replies in a queue, based on that order and reply them one by one. e.g. If the input: 'Hi, what is your name' => 'Hello there.', 'I cannot tell you my name'

A bit more in detail

I suggest you to take a look at the nltk library. This library offers a wide set of functions for natural language processing. In addition, it offers some cool classification, analysis and more functionality which allows you to do a lot more (advanced) AI related things.

But, the nltk library is especially useful in your case for the preprocessing step of the user_input.

you already performed lower-casing, and splitted on whitespaces. But actually there is a lot more and more powerful ways for preprocessing by using this library.

  • First of all you could tokenize the text, which will split the text in tokens. This is similar to your splitting on whitespaces.
  • You can perform stopword removal. Stop word removal will remove simple words from the input_text, words like 'the', 'a', etc. They don't give any significant meaning.
  • Next you can perform stemming, which will transform words like working, worked,works to it's stem form: work. This way you don't need multiple verb-forms in your subsets, only the stem will do.
  • Last and most important I would suggest to use n-grams, probably bigrams or trigrams, in addition to your unigrams. A bigram is every sequence of two adjacent elements in a string of tokens.

By performing this all, you can achieve the following:

user_input:

Hi, what is your name?

preprocessing will result in a list of words/tokens:

[Hi, what, be, your,name, Hi_what, what_is, is_your, your_name] based on these you can build your subsets and change: [{"your", "name"}, ["My name is not known.", "I cannot tell you my name,"]], into [{"your_name"}, ["My name is not known.", "I cannot tell you my name,"]],

I only touched these things briefly and you probably need to take a further look into it yourself. But this could make your "simple" AI, a bit more "advanced". ;)

Good Luck!

\$\endgroup\$
4
\$\begingroup\$

Accuracy in the docs

I am looking at

"""
Obtains user input. This function 
removes all punctuation, lowers text, 
splits on whitespace, and returns a
set of all the unique elements. An
example input value could be:

    "Hello, hello, there, there"

And the return value would be:

    ["hello", "there"]
"""

Except that the return value is not that. It is a set, so It should have curly braces around.

To avoid such inaccuracies and test your code at the same time, I suggest you use doctest (just type import doctest and help(doctest) at the interactive shell to learn more).

One-liner

The function obtain_user_input is nice and spread out but in Python such a trivial function is usually written in one or two lines to avoid distracting temp variables (or reassignements).

\$\endgroup\$
1
\$\begingroup\$
from re import sub as rsub

[...]

def obtain_user_input(prompt: str) -> set:
    [...]
    user_input = rsub("[^a-zA-Z0-9\s]*", user_input, "")

My first reaction was: "What's rsub? hmm... Oh, maybe it was imported". It takes 3-4 seconds to figure out what the function you are calling is. Why not just do

import re

[...]

def obtain_user_input(prompt: str) -> set:
    [...]
    user_input = re.sub("[^a-zA-Z0-9\s]*", user_input, "")

The import is much shorter + the call is very explicit at the cost of only 2 additional characters...


from random import choice

[...]

def get_replies(user_input: set) -> str:
   [...]
            return choice(possible_reply[1])

Similar, but worse. The only reason choice() makes sense to me is that I use random.choice() so much in my programs.

random.choice() reads so perfectly that shortening it should be a crime.

Basically, you are asking the reader to map choice() to random.choice() in their head.


def obtain_user_input(prompt: str) -> set:
    """
    Obtains user input. This function 
    removes all punctuation, lowers text, 
    splits on whitespace, and returns a
    set of all the unique elements. An
    example input value could be:

        "Hello, hello, there, there"

    And the return value would be:

        ["hello", "there"]
    """
    user_input = input(prompt)
    user_input = rsub("[^a-zA-Z0-9\s]*", user_input, "")
    user_input = user_input.lower()
    user_input = user_input.split(" ")
    return set(user_input)

The docstring is way longer than the code, which should be shorter anyways.

def obtain_user_input(prompt: str) -> set:
    user_input = input(prompt)
    user_input = rsub("[^a-zA-Z0-9\s]*", user_input, "")
    return set(user_input.lower().split(" "))

I don't see a need to put a docstring on such a simple function.

\$\endgroup\$
  • 1
    \$\begingroup\$ I only import choice from random because there's no need to include everything from the module. \$\endgroup\$ – Ethan Bierlein Jul 22 '15 at 16:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.