In response generation a semantic expression is turned into a natural language sentence in order to be shown to the user. It is based on rewrite rules and data source data.
An application uses the relation out to store the information that should be output to the user, in the form of a tuple that starts with a output type. The generator uses the relation last_output to read the last output tuple available.
Here's an example of a semantic expression the application whats to output. It's a clariying expression that asks the user for information.
out('how_many', E1, E2)this expression can be handled by this rule
{
"syn": "s() -> 'How many' thing(E1) 'per' thing(E2)+'?'",
"if": [Atom("last_output", "how_many", E1, E2)],
},Generation takes place based on rewrite rules that take a category as a head, and one or more categories as body. In the following example the head is s and the body categories np and vp.
Each sentence starts out with a s category. The generator looks for rules with s as its head and also match the if condition. The first rule (from top to bottom) that matches is used. The others are skipped. The sentence that first looks something like "s()" is now rewritten to something like "np() vp()", and the np and vp will be rewritten further.
{
"syn": "s() -> np(E2) vp(E1)",
"if": [
Atom("last_output", "declarative", E1, E2),
],
},Note that the variables E1 and E2 that are bound during the condition matching process, are used as arguments to the np and vp categories, and will be passed to the new rules as input.
vp may rewrite to verb and verb can have a rule like this
{
"syn": "verb(E1) -> 'starts'",
"if": [
Atom("start", E1, E2),
],
},which produces the word starts.
A value that is to be displayed as-is, can be produced using the category literal.
{
"syn": "s() -> 'The answer is' literal(E1)",
"if": [Atom("last_output", "count", E1)],
},You can use the category format to use a custom function to create a result. This function is provided in the rule via the key "format".
{
"syn": "things(E1) -> format(E1)",
"format": lambda things: format_list(things)
},When a variable contains a list of values, and each of the values needs to be rewritten separately, you can use the list_element and list_combine items.
{
"syn": "s() -> np(E1)",
"if": [Atom("last_output", "list", E1)],
"list_element": E1,
"list_combine": lambda nps: ", ".join(nps),
},In this example the result is an enumeration of names, separated by commas.
The combine function tells the generator how the results of the body nodes are to be combined. Without a combine function, the results are cast to strings, any None values removed, and concatenated. The combine function may change that:
{
"syn": "s() -> format(E1, E2)",
"if": [Atom("last_output", "table", E1, E2)],
"format": lambda results, units: format_table(results, units),
"combine": lambda table: table,
},Here the consequents (the output of format) are not turned into a string, but just passed on as an array.
post is executed last and adds simple string based processing, like strip.
{
"syn": "s() -> custom()",
"post": lambda custom: custom.strip()
},At some point you may want to know exactly what happens in what order. The generator processes a rule like this:
- match the head along with its arguments
- execute the
ifclause and keep the variable binding - if the rule contains a
list_elementvariable, this variable is assumed to be a list, and the following steps are repeated for each element in the list. Together they will form alist_result. Otherwise, they are executed just once- each of the consequent categories is processed in a recursive way, by this same process
- the result of these consequents are combined by the
combinefunction, if available. Otherwise anyNonevalues are removed, they're converted to strings, and concatenated. - the resulting string is postprocessed by a
postfunction, if available
- if the rule contains a
list_elementvariable, and alist_combine, thelist_combinetakes thelist_resultas input and return the output.