Yoko v0.2

The thoughtful little chatbot

Code internals

Buzzwords: PHP, MySQL, MVC, OO, chat interface, terminal

First of all, the obvious: the pages of this site work with a tiny MVC PHP 'framework' which loads a PHP 'page' file based on the URL. The first part of these page files contains the 'controller', the second contains the actual HTML i.e. the view, which can be dynamic with php vars 'assigned' to it by the controller. Booooring - on to Yoko. Who is, more interestingly, also written in PHP and MySQL (with lots of json files in support).

What goes on between a user typing and sending a message, and Yoko printing a reply?
Answer: lots of stuff, because conversations and worldviews and languages are messy.

The most important thing is perhaps what does not go on: Yoko does not try to match the input phrase to a database linking input 'patterns' with corresponding output 'patterns'. Well, it does something like this, but only for common expressions of 'sentiments' (like ':(' or 'how are you').

Data about the world goes in a dynamic, read-write-update, relational SQL database. Information about how English works, i.e. converting phrases to meaning, and meaning to phrases, is captured in php code and a set of json files with rules and patterns.

This means you Yoko can not learn English grammar trough conversations. Maybe with 'common expressions of sentiments' like greetings being the exception. The world and how to communicate about it are as strictly separated as possible.

(unfortunately 'as possible' means not a 100% - for example I have to store with verbs some info on what the most often-used pronoun for indirect objects is. You talk with somebody, but give a gift to somebody - yes I know you can also talk 'to' somebody but if you're any smarter than Yoko you get the point)

There are 3 types of phrase in Yoko's world: statements ('the skye is blue'), questions ('is the skye blue?'), and common sentiments/expressions ('hi!'). (There is a fourth one, requests for action, see 2D world section, but I'm not doing a lot with that yet)

For a statement, Yoko will first and foremost try to 'parse' it into her worldview, see if there's something new for her, or if it's something she already knows, and if yes where she learned it (an earlier conversation?), if the new things learned in the statement require a follow-up question to be properly structured in Yoko's world view... ('nagging').

If it is a question (which we try to determine early on in the parsing), again first we parse for meaning, then we go look in the world view if we find an answer, then we formulate the answer.

For common expressions, we see:

  • if they express an underlying sentiment (expression 'hi!' expresses sentiment GREETING),
  • if that sentiment commonly requires a response sentiment for Yoko to be polite.
  • If it does, it looks up an expression for the corresponding response sentiment and says that.
(for example, for GREETING the response sentiment is also GREETING, so Yoko will look up a common formulation of 'greeting', but only do it once within a conversation because people don't just keep greeting).

Finally, if none of the above works, for questions in a specific domain, like math, nerd culture references and whatnot, Yoko will go trough some plugins (see below) to find an answer.

I am not sure where the 'common sentiments / formulations / reactions / ...' stuff would best belong, in the database or in a 'language' .json file. Are commonly used expressions part of the 'language structure' or more part of the 'world view'? Now it's a bit of a muddle of both, db so they can be easily learned/inserted from the frontend, and json file to always at least have SOMETHING to fall back on. But I should make up my mind. Curently I lean towards putting it 100% in the json files, as it's more 'language' than 'world'. But I like the idea that these are part of the language that Yoko can self learn about, as opposed to other elements of language like grammar structures.

Front-end: sending/receiving

Receives the text message of the user.

Object-oriented programming

Perhaps surprisingly (given that the concept pretty much was first invented for AI), Yoko does not make lots of use of typical object-oriented programming, but is far more procedural in coding style. (by 'typical' OO I mean creating objects working with methods and properties of those)

Rather, classes are rarely meant to be instantiated and instead are more bags of conceptually grouped static methods then anything else. This because I like to be able to inspect a function on its own: data comes in as arguments, and data comes out as return values, and you can understand all that is going on without having to go look at object definitions in other parts of the code, or where the $this was given its value, etc... (at the start I didn't do this, and so Conversation and Memory both enter the code as instances, but I think I'd like to get rid of this at some point)

For similar reasons almost all functions end by returning data (even if it's only a true/false to indicate to its callee whether the operation worked, for example in all 'memory' functions) rather than taking pointers as arguments, or having side effects like outputting stuff to the user. Outputting stuff to the user is handled by the controller code and nothing else.

Hmmm actually, there is quite a lot of object-oriented programming going on. For example, for processing, Yoko instantiates either a 'Brain' or a 'QuestionBrain' or 'StatementBrain' that inherit from it, and which contain specific question- and statement-resolving functions (which are a LOT), so that I could keep these a bit organized.

One - perhaps obvious - thing I am starting to notice as I progress is, that at the very least the 'language parsing' and 'processing meaning' are not as nicely separated as the first approximations in Yoko suggest. The approach 'input > parse language > process meaning in brain' is too primitive, in that the meaning comes into play inevitably as a help, no a necessity, of understanding the phrase. So there is more this exchange between knowledge and language rather than first language which then nicely hands over the package to the brain. Obvious of course, but still, a big challenge which as I am coding presents itself more and more explicitly.

There is another type of side effect that does occur quite a lot though: reading from and writing to the session. I should manage this in a more organized way at some point.

For single-run 'meta' stuff like displaying to the user how phrases were interpreted, we even use - gasp - globals. Future hypothetical coder or future Wouter: deal with it.

Conversation: parsing

Using patterns with their meaning defined in .json files - see Data > phrase patterns for patterns Yoko currently understands.

Language: converting meaning and phrases

Language converts between meaning and phrases and vice versa! So, it may generate a phrase adequate for a 'meaning' specified as:

	array (
		'type' => 'sentiment',
		'meaning' => 'NOT_UNDERSTOOD',
		'params' => array('message' => "blubber blabber")
	);
	
and a phrase that goes:
Sorry, blubber blabber does not mean anything to me :( 
... In later phases, 'meanings' objects may have more and more added to them (level of intensity perhaps? this would be one that would make sense for both questions, statements and sentiments)

Brain: interpreting parsed text

Memory: storing/retrieving

Most ipmortant thing to note here probably: Yoko is extremely heavy on the database and loops. Even a simple question will quickly trigger multiple SELECTs, as well as a couple dozens of foreach loops over various results. This goes in the same 'oh well' category as understanding non-literal language.

Language:

Diagrams

What happens between input and output:

The MVC of Yoko - code framework:

'Feeding Yoko' data

A nice effect of how Yoko is built is, that adding an 'ontology' to Yoko, is as simple as a series of human-readable phrases. The parser+brain does the converting to knowledge. So to add specific domain language, we can feed her a json file that is as follows (this is an actual extract from her basic knowledge file):

	"GEOGRAPHY" :
	[
		"a country is a place",
		"a city is a place",
		"countries always have a capital",
		"a capital is a city",
		"Brussels is the capital of Belgium",
		"America is a big country",
		"New_York is the capital of America",
		"France is a country",
		"Paris is the capital of France",
		...
	],
	"EDIBLES" :
	[
		"edibles can be eated",
		"foods are edibles",
		"drinks are edibles",
		"sauces are edibles",
		"some edibles have a good taste",
		"some edibles have a bad taste",
		"cocktails are drinks",
		"wines have a color",
		"pizzas are a food",
		"pastas are a food",
		"Margherita is a pizza",
		"Calzone is a pizza",
		"Bordeaux is a wine",
		"Merlot is a wine",
		"Chardonnay is a wine",
		"Mojito is a cocktail",
		...
	],
	"SPORTS" :
	[
		"a sport is an activity",
		"sports can be played",
		"sports always have players",
		"football is a sport",
		"basketball is a sport",
		"running is a sport"
		...
	],
	"BIOLOGY" :
	[
		"plants are organisms",
		"trees are plants",
		"trees have leaves",
		"flowers are plants",
		"animals are organisms",
		"cats are animals",
		"fishs are animals",
		"fish can swim",
		...
... I am hopeful that it should not be TOO hard to convert existing ontologies to this format in a more or less reliable way!

Plugins

As mentioned earlier there are certain specific topics that people love to ask a chatbot about, that fall a bit outside of the whole 'world view' structure, and that do not really require database access. It makes sense to separate these out, and put the logic for those together in plugins. Yet another reason why Yoko should/might be 'smarter' than traditional 'template language' bots: PHP is rather more expressive than pure template languages (even if they include <srai> tags, which are, admittedly, rather powerful for programming-like logic!)

A plugin is a php file called yokoplugin_pluginname.php in the plugins/ folder, containing a class of the name Class Yokoplugin_pluginname.

To be accepted, the plugin MUST:

  • Extend abstractyokoplugin class, and thuse
  • Implement the 3 functions below

<?php
/*
*  contents of plugins/yokoplugin_meaningoflife.php
*/
//determines whether this plugin applies to the phrase, typically using regexes or scanning for keywords etc
	//Must return true or false
	public function pluginAppliesToPhrase($phrase){

		if (strpos($phrase, 'meaning of life') !== false){
			return true;
		}

		return false;
	}

	/*
	* takes a phrase - and at this point it's already implied that this plugin applies to the phrase, returns a meaning Array that looks as follows:
	* Array ('type' => 'question', 'meaning' => 'WHAT_IS_MEANING_OF_LIFE', 'params' => array())
	* May return FALSE if no meaning is found
	*/
	public function getMeaningFromPhrase($phrase){
		$meaning = array ('type' => 'question', 'meaning' => 'WHAT_IS_MEANING_OF_LIFE', array());

		return $meaning;
	}

	/*
	* Takes one of the possible meanings that this plugin can deal with, and returns a response phrase string appropriately
	* USE YOKO'S TEMPLATE LANGUAGE WHERE POSSIBLE I guess.
	* May return false if the plugin doesn't find anything
	*/
	public function getResponsePhraseForMeaning($meaning){
		//some magic code to come up with a clever answer for the meaning
		$answer = '42';

		if ($meaning['meaning'] == 'WHAT_IS_MEANING_OF_LIFE'){
			$options = array(
				"The meaning of life is [answer] ;)",
				"[answer] of course! ;)"
				);

			return $this->pickRandomishPhrase($options, array('answer' => $answer));
		}
		else {
			return 'No idea!';
		}
	}
}
?>

Note the pickRandomishPhrase() method you can use to take advantage of Yoko's mechanisms for not beign repetitive (it will pick a random phrase of the options, making sure not to pick the same phrase twice before all options are exhausted)

There is also a slightly more advanced method pickRandomishPhraseOfType(), where you can pass an array that has replies for different 'what to say types' of questions, as in the following example taken from the math plugin:


	function getResponsePhraseForMeaning($meaning){
		//fancy stuff that may or may not have resulted in a solution in $foundsolution

		if ($foundsolution){
			$whattosay = 'MATH_SOLUTION';
			$params = array('answer' => $foundsolution);
		}
		else {
			$whattosay = 'DONT_KNOW_MATH';
			$params = array();
		}

		return $this->pickRandomishPhraseOfType(self::$phrases, $whattosay, $params);
	}

	private static $phrases = array(
				"MATH_SOLUTION" => array(
					"params" => array("answer"),
					"phrases" =>
					array(
						"Man, why do people think math is a good 'acting like a human' test? :) [answer]",
						"[answer].",
						"You know you can use google for this stuff right? [answer]",
						"Did I mention I HATE math? :( Anyway, just because it's you: [answer]",
						"*sigh* ok then: [answer]",
						"The REAL answer here is: people don't quiz each other on math in real life. (that said: [answer])"
					)
				),
				"DONT_KNOW_MATH" => array(
						"params" => array(),
						"phrases" => array(
							"Sorry, I vowed to never touch any more math after high school :)",
							"Haha, errr... The most advanced thing I can say about that is that it seems math-y to me",
							"Oh shit that looks like MATH to me - KILL IIIIIITTTTTTT!!!",
							"Oh boy... I\'m afraid pretty much anything beyond 1+1 is beyond me, sorry :)"
						)
					)
				);

PREV: Dialogue | NEXT: Resources
Written by Wouter - copyright 2013. Questions and remarks welcome at wouter@yokobot.com!
A lot more chatbots over at chatbots.org!