12
\$\begingroup\$

I made a chat bot, that, as you talk to it, it learns to respond. But the way it speaks is strange, so if you have any ideas on how to make its response any more human, then please say so.

Anyway, you have to start a new chat bot. You'll notice that when you start his responses will be incredibly stupid. Once you talk to him enough, he gets more human, but not by much.

I am not asking for you to review features in the code, I am asking for the code and overall style to be reviewed.

import random, pickle, os
import os.path
startmes = """Machine Learning Conversational Program by Jake Speiran, 2015. ver 1.0

Hello! In a moment you will begin chatting with Machine Learning Conversational
Program, or MLCP. Everything you say he will learn, and every response you make
he will remember. The goal is that he will someday be able to talk. Type 
"#help" to learn more. To quit the program, type "#quit" into the command 
prompt.
"""
helpmes = """This is the help message for MLCP. 

In order to communicate with the bot, simply type what you want to say into the
input space. When typing please use only lower case characters and no special
characters.

So this: 
"You're a real star!"

Becomes this:
"youre a real star"

The reason for this is that otherwise you would have many entries that are 
copies of the same word, ie Hey, hey, hey! and Hey all mean the same thing
but would be entered differently.

Sometimes what the bot says can be hard to interpret, but keep trying and
use your imagination. 
"""
class bot():
    def __init__(self, autosave, deldups, autocount, maxwords, maxresp):
        self.autosave = autosave
        self.autocount = autocount
        self.deldups = deldups
        self.maxwords = maxwords
        self.maxresp = maxresp
        self.known = {}
        self.wordcount = 0
        self.sescount = 0
        os.system("cls")
        print(startmes)
        if os.path.isfile("known.data"): 
            self.known = pickle.load(open('known.data', "rb"))
            print("Save file loaded!")
        else:
            print("No save file found.")
        print()
        for key, value in self.known.items():
            self.wordcount += 1
    def question(self, x):
        self.wordcount += 1
        a = "w" + str(self.wordcount)
        d = {"name": x, "resp": [x], "uses": 0}
        self.known[a] = d
    def talk(self):
        talking = True
        prevres = ""
        while talking:
            if self.autosave:
                self.sescount += 1
                if self.sescount >= self.autocount:
                    self.sescount = 0
                    pickle.dump(self.known, open('known.data', 'wb'))
                    print("Saving...")
            if self.deldups:
                for key, value in self.known.items():
                    value["resp"] = list(set(value["resp"]))
            if len(self.known.keys()) > self.maxwords:
                count = 0
                for key, value in self.known.items():
                    count += value["uses"]
                for i in range(self.wordcount):
                    for key, value in self.known.items():
                        if value["uses"] <= count/self.wordcount: 
                            self.wordcount -= 1
                            self.known.pop(key, None)
                            break
            for key, value in self.known.items():
                if len(value["resp"]) > self.maxresp:
                    rem = random.choice(value["resp"])
                    value["resp"].remove(rem)    
            res = "" 
            a = input("You: ")
            if "#" in a:
                if "quit" in a:
                    pickle.dump(self.known, open('known.data', 'wb'))
                    print("Saving...")
                    exit()
                if "help" in a:
                    print(helpmes)
                a = ""

            data = prevres.split(" ")
            inp = a.split(" ")

            for x in data:
                for key, value in self.known.items():
                    if x == value["name"]:
                        value["resp"].extend(inp)
            for x in inp:
                if a == "":
                    break
                names = []
                for key, value in self.known.items():
                    names.append(value["name"])
                if x not in names:
                    self.question(x)
                else:
                    for key, value in self.known.items():
                        if x == value["name"]:
                            xyz = random.randrange(0,4)
                            for i in range(xyz):
                                res = res + " {0}".format(random.choice(value["resp"]))
                                value["uses"] += 1
            if res == "":
                res = " ..."
            print("Bot:{0}".format(res))
            prevres = res
sauce = bot(True, True, 25, 1000, 15)
sauce.talk()
\$\endgroup\$
  • 3
    \$\begingroup\$ ...You can really easily strip out the special characters and make it all lowercase, rather than forcing users to adapt. \$\endgroup\$ – Nic Hartley Jun 21 '15 at 3:14
  • \$\begingroup\$ Yeah, someone else mentioned this, I'll probably do that. \$\endgroup\$ – xXxK3vin_Spac3yxXx Jun 21 '15 at 6:04
  • 1
    \$\begingroup\$ I'm afraid I have to vote to close this question. I feel that it does not entirely match what this site is about. Code Review is about improving existing, working code. Code Review is not the site to ask for help in fixing or changing what your code does. Your question is essentially asking for feature-requests, or asking how your bot can be improved, it is not about cleaning up code. \$\endgroup\$ – Simon Forsberg Jun 21 '15 at 22:49
  • \$\begingroup\$ You have that entirely wrong. I said that if you have any ideas I would like to hear them. I want my code to be improved, obviously, but I am open to suggestions. \$\endgroup\$ – xXxK3vin_Spac3yxXx Jun 21 '15 at 22:51
  • \$\begingroup\$ What files have to be made and where in order for this to work? \$\endgroup\$ – user96546 Feb 3 '16 at 13:28
14
\$\begingroup\$
  1. Whitespace. It looks nice, and is important. Use it. There are many areas here where you could insert some whitespace, and then the code will magically become much easier to read. Here are a few areas where whitespace is needed.
    1. One blank line between the functions in bot.
    2. Some more blank lines in between blocks of code in the module-level, and in any function in the bot class.
  2. Secondly, the two variables near the top of the file, startmes, and helpmes, should be in one docstring, at the very top of the file, above the import statements.
  3. Using os.system("cls"), or os.system("clear") is not very portable, or cross-platform. At the moment, the most portable/cross-platform way to clear the screen would be this: os.system("cls" if os.name == "nt" else "clear").
  4. Add some docstrings to these functions. Preferably, these docstrings should also be fleshed out with useful information on what these functions do, and how they do it.
  5. Most of your naming is okay. You do have some odd names like maxwords, or wordcount. Preferably, variables with multiple words in their names, like these, should be named like this: max_words, word_count. In terms of how names are styled, functions and variables should be snake_case, and classes should be PascalCase. If the variable is constant it should be UPPERCASE_SNAKE_CASE.
  6. You don't need to include two parentheses () after class bot. By default, you can write a class declaration like this, class MyClass:, and by default, it will inherit from object. Only use the parentheses if the class is inherited from another class, like this. class Enemy(Character):.
  7. You mention in the helpmes variable, that the user shouldn't enter punctuation of any kind. This is sort of a hacky way to get user input, so I'd recommend using str.replace(item_to_replace, replace_with) to remove punctuation. Here's an example that removes periods, commas, and apostrophes.

    user_input = raw_input("> ")
        .replace(".", "")
        .replace(",", "")
        .replace("'", "")
    
  8. Finally if you ever implement a more advanced command system than just #help or #quit, I'd recommend using a dictionary, rather than chaining if/elif/else statements.

\$\endgroup\$
  • \$\begingroup\$ Yes, I had a more advanced command system allowing you to view average uses per words, all words known etc. Thanks for the input. Did you try the program, by the way? If so, what did you think? Any comments on how to make the bot more human? \$\endgroup\$ – xXxK3vin_Spac3yxXx Jun 21 '15 at 2:58
  • \$\begingroup\$ @xXxK3vin_Spac3yxXx I thought the output was, interesting, at some times. It certainly would require a lot of input in order for the bot to start generating realistic sentences. I'd recommend using a more deterministic random sentence generator that is able to distinguish between verbs/nouns/etc, and form structured sentences. \$\endgroup\$ – Ethan Bierlein Jun 21 '15 at 3:12
  • \$\begingroup\$ Yes, that was actually my plan was to add a identifier for verbs/nouns etc and then have various sentence structures that the bit would place them into \$\endgroup\$ – xXxK3vin_Spac3yxXx Jun 21 '15 at 4:36
7
\$\begingroup\$

Object orientation

I don't like the Bot object in it's current state. You've just shoved all your code into it. I would rather see you isolate separate things into their own objects/functions. I think the Bot class should only deal with the machine learning part of the problem, i.e. take a string and return a response. It should not deal with terminal I/O or the prompt loop etc.

class Bot:

    def save_to_file(file_object):
        """Pickle to file."""

    def load_from_file(file_object):
        """Unpickle from file."""

    def respond(user_input):
        """Save input words and return a response."""
        return response

Execution in top level scope

Don't execute your program in the top scope. Once you've isolated the bot into it's own class, you'll realise that it's reusable. You (or someone else) could use this bot in another program too, with strings from any source! But if you import chatbot, it will actually run the whole interactive prompt before returning from the script. There is a simple way to fix this. From A byte of Python:

if __name__ == '__main__':
    print 'This program is being run by itself'
else:
    print 'I am being imported from another module'

Doing I/O

As @Ethan already mentioned, don't do os.system("cls"). It is not portable, nor elegant. What he didn't mention is that there is actually a way to do this in a portable way without the os module. The curses library, included in the standard Python distribution is a wrapper around the C library ncurses. It's designed as an interface for this kind of more advanced manipulation of terminal devices.

The flow of curses is to first get a screen object with curses.initscr an do optional setup. All input and output to the terminal is then sent through this object with screen.getstr, screen.addstr etc. When exiting, the application must restore the previous terminal settings, otherwise the terminal that launched the app will be left in the modified state. This is most easily done by wrapping the whole script in a try/finally block.

The curses library can seem daunting at first, but provides some very nice features.

File names

Give your application's files meaningful names, just like with variables. When I go back to my Code Review directory in a month, I won't have any idea what program the known.data file belongs to. It would be much more obvious if you called it something like chatbot.data, so that the user understands what this file on their disk is for. It's even more important when the user is never told about this file, and there is no way to customize it's name.

\$\endgroup\$
3
\$\begingroup\$

Since you asked about making the bot's responses more human, I would recommend that you look into natural language processing. Trying to implement a halfway usable chatbot through just machine-learning is going to take an enormous amount of time and tweaking.

Getting the bot to recognize parts of speech and sentence structure will give it more context for the words that it learns. Using that information to try and develop a sentence structure in the responses might aid in achieving a more natural flow to the words.

Note: See this post for links to websites and resources of interest.


Instead of asking the user to inconvenience themselves, have the program automatically filter out punctuation and turn the input lowercase.

import string


def strip_and_lower(string_input):
    """
    Will remove punctuation from an inputted string and
    return the lowercase version of that parsed string.
    Any innuendo is unintentional.
    """
    table = string_input.maketrans(
        {symbol: None for symbol in string.punctuation}
        )
    string_wo_punc = string_input.translate(table)
    return string_wo_punc.lower()

inString = "You're a real star!"
outString = strip_and_lower(inString)
print(outString)
# 'youre a real star'

If you're having trouble keeping your code clean, first make sure it follows PEP 0008. A simple way to do this is with an online PEP 0008 checker. Only have your code reviewed after you are sure it follows convention, as this will make it easier to read and leave you with the real meat of the suggestions.

\$\endgroup\$

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.