The truth is that it would be a great feature to have, for lulz at least. And besides it's a great part of the history of computing. :) It also forces us to re-evaluate the design a bit as the bot has to be able to run as an entity of its own.
Instead of coming up with my own implementation of Eliza, I'm going to use an existing one I found here. Even though the code is old, it works after the references to a deprecated module, whrandom, are replaced with references to random.
I'm going to start out by writing a scenario describing how Eliza should work user interface wise. As it's not quite possible to predict what she will answer, I will define concept of ellipsis (...) which allows the input to be anything. After this I will look into encapsulating the bot in a command of its own and make the adjustments needed to the interpreter.
Scenario for Eliza
I expect that Eliza should work like this:
Note that I added the assignment part there intentionally just to exercise the context change.
If you run the tests now, this one will fail at the first ellipsis. This is something we need to add to the scenario tester. Fortunately that's easy enough to add there. You can find my implementation here.
Let's try running all tests now. Unfortunately they all pass! The current definition of the scenario is too generic. There is a way past this problem, however. It's just a matter of making Eliza greet a standard way. The good old "Hello. How are you feeling today?" will do just fine. Here's a scenario that contains a proper greeting:
Hooray! It fails. Now it's a good time to take a look at what the command should look like.
Command for Eliza
You can find my initial implementation of Eliza here. I renamed the original Eliza module to pyeliza and updated it to use random module instead of whrandom. As before, the files belong to commands (in /commands/eliza to be precise).
I did not bother mocking the response of the therapist as it wouldn't have done much good in this case. I did mock the behavior of the context, however. This gives us nice constraints based on which to come up with tests for the interpreter:
- It should be possible to provide context to the execute method as a parameter
- Context should contain "claim_for" method that may be used to claim it
- Context should contain "release" method that returns its state to None
- Context should contain "owner" attribute
You can find my implementation of these specifications here.
Running all tests now gives a new error, "KeyError: eliza", to sort out. Also the stdout of py.test contains a helpful clue stating that "No module named pyeliza". Apparently it fails to load the newly added Eliza command properly as it cannot access the pyeliza module.
This can be sorted out by making sure the command directory is added to Python path variable just before trying to import it. You can find my fix for the issue here. I know the fix may add duplicate elements to the sys.path list. It might be better to handle the whole issue later by implementing a class that wraps sys.path and makes manipulating it easier.
Apparently the fix wasn't enough as the Eliza scenario still fails. Now with MatchError, however. It states that it receives 'null' instead of a greeting that was expected. Curiously if the test is test ran on its own ("py.test -k test_eliza"), it passes! As the solution to this issue is a bit non-obvious, I'm going to explain the main steps I took while coming up with one:
- Check interpreter exception. -> This gave NameError, name 'eliza' is not defined. The exception was raised by the Python command which implies that the Eliza command was not not matched properly (perhaps something wrong with aliases?).
- Check plugin loader. I added a debug print showing me the class that was loaded (print plugin_class). -> Curiously the name of the Eliza command was not capitalized unlike the other.
- Check the command. Looking at the implementation gave me a better idea of what was going on. It appears that "import pyeliza" caused another eliza class to appear into the namespace introspected by the inspect module. This combined with the fact that my class access (file.classes) is done in lowercase, caused the issue to arise. The lowercased eliza simply shadowed the real Eliza.
Peeking at file.py gave me the answer. The way I load modules accidentally overwrote the original Eliza module as I had given both modules the same name. I fixed this issue by changing "module = imp.load_source('', path)" to "module = imp.load_source(os.path.basename(path), path)".
Fixing Absolute Import Errors
These changes make the tests pass in flying colors. Sadly actually running the leads to some errors (example: "Parent module 'eliza' not found while handling absolute import") although it looks like it's otherwise totally functional. The only cases (assignment, eliza) in which this error appears happen to be modules that depend on external modules.
Fortunately the fix is simple, albeit a bit ugly. The solution is to use new absolute imports by importing them from the future. Simply adding "from __future__ import absolute_import" at the top of assignment.py and eliza.py enable the kind of behavior we happen to need in this case. Further discussion about it may be found here.
It's possible to run the interpreter without any errors now. It seems that Eliza responds with greeting always, however. Here's my fix for the issue. It simply tested the context in a wrong way which caused the bug to remain there.
Finally we have a proper therapist in place. Math sessions in Placidity should be much more fun now. Along the way we shaped the architecture further and fixed a few issues. I will look into implementing a "quit" command that overrides the Python one in the next part of the series.
You may find the source code of this part here.