This is the first version of the Dialogue Manager Script (DMS) documentation, which will demonstrate the current state and capabilities of DMS through a tutorial, outline the syntax and semantics, and discuss how to write effective DMS. DMS is a high-level language that compiles down to [[DMPL]] compliant JSON.
The code snippets presented in this page can be evaluated by hitting the "Run" button, which compiles DM Script into [[DMPL]] JSON, and then processes that JSON on a JavaScript runtime. The compiler can be found on GitHub [[dms-compiler]].
This guessing game chat-bot will help us better understand DMS.
Let's build a simple prompt that asks the user for some input.
We start by printing out messages using act
, and
then awaiting the user's input.
act "Guess the number!" act "Please input your guess." input -> guess { _ { act "You guessed " + guess } }
The input ->
statement awaits for user input, and feeds it to a variable of our choosing.
In this case, the variable guess
is populated with the user's input.
once { act "Guess the number!" secret_number = pick(range(1, 101)) } act "Please input your guess." input -> guess { to_num(guess) > secret_number { act "Too big!" } to_num(guess) < secret_number { act "Too small!" } _ { act "You win!" pop true } }
pop true
, which
pops the current script from the runtime stack.
The DMS syntax is loosely based off Rust.
Curly-brackets { }
establish a block of code,
statements do not need a trailing semi-colon ;
,
and labels before the curly-brackets annotate the behavior of the block.
Similar to ECMAScript, DMS is not a strongly typed language.
Scripts written in DM Script are also called components. The runtime shall evaluate the component in an infinite loop, similar to game-loops inherent in most game engines. Components may call other components through run or use statements.
Programmers may leave notes, or comments, in their code. These comments are ignored by the compiler, but are useful for other programmers reading the code.
// Write your comments here.
In programming, a variable is nothing more than a placeholder for some value.
The strings “Chai”
, and “Moo”
are simply values that a
placeholder dog_name
could take on,
as can be seen in the following example.
// Initial value for a dog name. dog_name = "Chai" // Later on dog_name can be changed to a different value if needed. dog_name = "Moo"
Note that in DMS, there is always an implicit infinite loop wrapping our code.
During execution it will keep creating and assigning a variable
named dog_name
the values "Chai”
and "Moo”
.
In order to remove these redundant variable creation and assignments
we can wrap code in a block beginning with the keyword once
.
The once keyword tells the compiler that the variable creation and
assignments should only be executed one time over the lifetime of the component.
once { dog_name = "Chai" } dog_name = "Moo"
Deciding whether or not to run some code depending on if a condition is true is a basic building block in most programming languages. The most common construct thats allows programmers to control the flow of execution of DMS code are if statements. Recall that in DMS there is always an implicit infinite loop wrapping our code, meaning that loops are inherently built in, and any code outside of a once block will be repeatedly executed over the lifetime of our program.
An if
statement allows us to branch our code depending on conditions.
These statements start with the keyword if
, and
follow up with a condition that evaluates to a Boolean value.
once { number = 1 } if number >= 2 { act "Number is greater than or equal to 2." } else { act "Number is less than 2." } number = number + 1
We can link multiple conditions by combining if
and else
in an else if
block. For example:
once { number = 7 } if number % 3 == 0 { act "Number is divisible by 3." } else if number % 2 == 0 { act "Number is divisible by 2." } else { act "Number is not divisible by 2 or 3." }
Forks are generalized if statements. They're the heart and soul of DM Script. As a refresher, let's consider the trivial example below of branching logic.
if true { act "Let's talk about animals." } else { act "Let's talk about colors." }
A fork
statement allows a more general way of representing branching behavior.
First, let's recreate the example above using fork
.
Think of it as a fork in the road, where we can only go down one candidate path.
Each candidate has an entry-condition.
In the example below, notice that the underscore _
is a shortcut for the Boolean value true
.
fork { _ { act "Let's talk about animals." } _ { act "Let's talk about colors." } }
By default, a fork
picks the first path whose entry-condition is met,
from top to bottom.
This method for resolving a fork is also called the "greedy" strategy,
because it picks the first possible candidate instead of considering all candidates.
Forks allow powerful customization of the branching behavior.
In the example below, the fork
picks a child-block at random.
We can optionaly change the fork-strategy by providing a dictionary
in a decorator #{}
directly before the statement.
In the example below, a strategy of {depth: 0}
is associated with the fork
.
#{depth: 0} fork { _ { act "Let's talk about animals." } _ { act "Let's talk about colors." } }
The content of the dictionary specifying the fork-strategy depends
on what the underlying runtime supports.
In this case, {depth: 0}
means use bounded depth-first search (BDFS),
with a depth of 0
to resolve the fork.
A depth of 0
in BDFS is effectively picking a candidate at random.
Different run-times may support different search algorithms,
such as Monte Carlo Tree Search.
The heuristic function for the search algorithm is specified by declaring a model.
The model of a fork-strategy is a list of lists, representing preferences.
For example, the pair [{x: 0}, {x:10}]
, declares that the value of x
is preferred to be 0
as opposed to 10
.
Internally, the runtime infers a utility-function, which is a real-valued function over the
variables in DM Script, that provides a total-ordering of preferences.
once { x = 5 } #{depth: 1, model: [[{x: 0}, {x: 10}]]} fork { _ { x = x + 1 } _ { x = x - 1 } }
Changing the preference model changes the program behavior.
For example, by swapping the pair [{x: 10}, {x: 0}]
,
the program now counts up instead of counting down.
once { x = 5 } #{depth: 1, model: [[{x: 10}, {x: 0}]]} fork { _ { x = x + 1 } _ { x = x - 1 } }
If we want to say that the value of x
should be 5
,
we can write the model as a list of preference-pairs, as follows.
once { x = 3 } #{depth: 1, model: [[{x: 5}, {x: 0}], [{x: 5}, {x: 10}]]} fork { _ { x = x + 1 } _ { x = x - 1 } }
Consider the following scenario, where a autonomous agent has 3 possible actions,
(1) eat food, (2) do nothing, or (3) work for food.
In DM Script, we can use fork
to define
the pre-conditions and outcomes of each action.
We specify that {health: 10}
is desirable over {health: -10}
,
and use BDFS to resolve the fork, with a depth of 1
.
once { health = 2 food = 1 } #{depth: 1, model: [[{health: 10}, {health: -10}]]} fork { food > 0 { act "eat" food = food - 1 health = health + 3 } _ { act "do nothing" health = health - 1 } _ { act "work" food = food + 1 health = health - 2 } }
Unfortunately, the program chooses the suboptimal sequence of actions:
it chooses to do nothing after eating up all the available food.
Let's increase its intelligence by changing the fork-strategy to {depth: 3}
.
Notice that now, the program will alternate between eating food and working,
which is the optimal strategy.
once { health = 2 food = 1 } #{depth: 3, model: [[{health: 10}, {health: -10}]]} fork { food > 0 { act "eat" food = food - 1 health = health + 3 } _ { act "do nothing" health = health - 1 } _ { act "work" food = food + 1 health = health - 2 } }
We'll cover more interesting use-cases of fork
in the effective DMS section.
The example in the variables section only allowed the variable dog_name
to take on string values.
However, DMS is a dynamically typed language meaning that variables can
take on any basic data type such as string
, float
, and
Boolean.
Consider the following example where we have a variable named
current_thought
which denotes what a programmer might be
thinking about throughout the day.
once { // Early in the morning their first thought could potentially be current_thought = "coffee" // Next they could be thinking, do I want office coffee? current_thought = false // Note that we don't use double quotes when assigning a boolean value to a variable. // And so they decide to buy coffee elsewhere, which has an associated cost. current_thought = 5.20 }
The primitive types defined in the previous section are all atomic literals. DMS also allows programmers to build more complex structures such as lists, or dictionaries.
A list
is an ordered arrangement of other structures.
All elements are enclosed within square-brackets [ ]
,
and separated by commas as follows.
once { // list of integers student_grades = [87, 90, 88, 92, 93] // list of strings student_names = ["Nawar", "Tom", "Chris"] // list of arbitrary types a_bunch_of_stuff = [false, 1, "two", [3, 4, 5]] }
A dictionary
is a structure that maps data in key-value pairs.
The value of a corresponding key can be any structure.
once { student_grades = { Nishant: 0, Carol: 93, Daniel: 90 } }
Take note of the syntax that was used to create the dictionary
in the above code snippet.
The keys of the dictionary
are symbols which are a finite sequence
of characters without the double quotation-marks,
and the values in this example are simply integer values.
Note that when accessing the values of a dictionary
,
the keys must be enclosed within double quotation marks.
once { student_grades = { Nishant: 0, Carol: 93, Daniel: 90 } act student_grades["Nishant"] }
Custom function definitions in DMS start with the keyword def
and have a set of
parentheses after the function name.
The curly-brackets { }
tell the compiler where the function body begins and ends.
Lastly, a pop
statement returns the value to be used by the caller.
Consider the following function which simply returns the string, "Greetings!"
.
def greet() { pop "Greetings!" }
Our declaration of the greet
function begins with the keyword def
,
is followed by the function name greet
, and ends with a set of empty parentheses ()
.
The function body contains all the logic that is expected to be executed when calling the function.
In the above example, we simply return the string "Greetings!"
using the keyword pop.
In order to call our function and print the corresponding greeting, we use the keyword act
.
once { def greet() { pop "Greetings!" } act greet() }
Functions in DMS can also have parameters, which are special variables that are part of the function declaration. The following re-declaration of the greet function allows programmers to pass in a variable of their choice, such as the name of a user in the following example.
once { programmer = "Naitian" def greet(user) { pop "Greetings " + user } } act greet(programmer)
The variables introduced in a function are local variables that are
only accessible within the current scope, and they shadow the variables
defined in the outer scope.
In the example below, we define a variable called greetings
within a function, and later try to access that variable out of scope.
Undefined variables by default evaluate to null
.
once { programmer = "Naitian" def greet(user) { greetings = pick(["Greetings", "Howdy", "Sup"]) pop greetings + " " + user } act greet(programmer) act programmer act greetings }
You can also implement closure using functions. For example, in the following code-snippet we show how to define a function that raises a number to an exponent.
once { def powerOf(power) { def f(num) { pop pow(num, power) } pop f } square = powerOf(2) cube = powerOf(3) act square(3) act cube(3) }
In DMS, functions are pure, so act or input statements are not recommended in the body of user defined functions.
==
or !=
.
Compare numbers
with >
, >=
, <
, and <=
.
once { x = 1 y = 2 act x == y act x != y act x < y }
Boolean
functions such as &&
, ||
, and !
are supported.
once { x = true act x || false act (x && !x) == false }
numbers
are available, such as
+
, -
, *
, /
, %
, and floor
.
once { x = (1 + 2 * 3) / 4 y = (x + 0.25) % 2 z = floor(x) }
to_num
and to_str
operators allow changing types between numbers
and strings
.
once { number = 13.5 act "The number is " + to_str(number) guess = "10" act "You're off by..." act number - to_num(guess) }
The ++
operator concatenates two lists.
Note, on the other hand, the +
operator combines strings
.
once { act [1, 2, 3] ++ [4, 5, 6] }
The len
function returns the length of the given argument.
Possible argument types include: string
, list, or dictionary.
once { school_data = { student_names: ["Jeremy", "Earle", "Chad"], school_name: "Institute of Good Learning" } act len(school_data) act len(school_data["student_names"]) act len(school_data["student_names"][0]) }
The pick
function allows programmers to pseudo-randomly select an item from a list.
once { athlete_rankings = [ { lonzo: 2 }, { zion: 1 } ] } act pick(athlete_rankings)
The get
function allows users to select a specific element of a list.
In the above example if we wanted to retrieve the last element we could do so as follows.
act get(len(athlete_rankings) - 1, athlete_rankings)
Or, use the [ ]
notation, which is just a syntactic-sugar of get
, as shown below.
act athlete_rankings[len(athlete_rankings) - 1]
The range
function generates a list from the starting value (inclusive) to the ending value (exclusive).
once { act range(1, 11) act range(1, 5) ++ range(5, 11) }
An optional third argument specifies the step-size.
once { act range(1, 11, 2) }
The map
function iterates through a list passed in, and produces a new list with modified elements.
once { act map(to_str, [1, 2, 3]) }
Multiple lists may be passed in, as long as the operator passed in supports that number of arguments.
once { act map("+", [1, 2, 3], [4, 5, 6]) }
This opens up a lot of useful list manipulation techniques, such as implementing zip
below.
once { def zip(xs, ys) { def pair(x, y) { pop [x, y] } pop map(pair, xs, ys) } act zip([1, 2, 3], ["a", "b", "c"]) }
The foldl
function iterates through a list from the left to yield a value by
applying a given operator to each element of the list with an accumulator.
once { def sum(xs) { pop foldl("+", 0, xs) } act sum([1, 2, 3]) }
The sort
, shuffle
, and reverse
functions operate on lists.
once { xs = [4, 3, 2, 1] ys = shuffle(xs) act sort(ys) == xs act reverse(sort(ys)) == xs }
The in
function is an in-fix operator that checks if an element exists within a structure.
once { month = { days: range(1, 31), month: "June" } act "days" in month act 31 in month["days"] }
the edit
function performs a mutation on a dictionary variable based on the specified
key value pair. The general syntax is as follows: edit(dictionary, value, key)
. Note that
if the specified key does not exist, an entry will be create within the specified dictionary variable
with the specified value.
once { info = {name: "unknown", inventory: {belt: 1, jacket: 2}} act edit(info, "Bob", "name") }
The patch
function mutates a
dictionary variable based on instructions encoded by another dictionary.
The general syntax is as follows: patch(dictionary, key_values)
.
Note that like edit if patch
encounters a key that does not exist within the specified
dictionary variable, patch
with create an entry with the missing key and
corresponding value.
once { info = {name: "unknown", inventory: {belt: 1, jacket: 2}} act patch(info, {name: "Bob", inventory: {jacket: 3}}) }
The from_list
function converts a list of pairs (2-element lists) into a
dictionary.
once { table = [ ["glasses", 1], ["jacket", 2], ["shirt", 0] ] act from_list(table) }
The exists
function allows programmers to check whether the given variable has been defined or not.
if exists("count") { count = count + 1 } else { act "Initializing count to 0" count = 0 } act count
Note that because we haven't defined the variable count
anywhere,
our program defaults to the else
branch of the conditional statement.
The pow
function raises a base
number to a specified exponent
.
once { base = 2 exponent = 3 act pow(base, exponent) }
Recall that DMS compiles down to [[DMPL]] compliant [[JSON]], which is a tree-like structure. The hop statement allows programmers to revisit the ancestors of the current code block. Consider the following example, keeping in mind the notion of code blocks having ancestors.
act "What's the largest planet?" // <-- hop 3, comes back to here input -> result { // <-- hop 2, go to grandparent result == "jupiter" { act "Correct!" } _ { // <-- hop 1, go to parent act "Nope" act "Try again..." hop 3 } } pop true
The above example can be hard to understand at first sight so lets break it down. At a high-level the
component itself is the root of our tree structure. From there, code blocks and statements having equal scope
can be thought of as children of the root. In the above example, the act statement and the code block
following the input
statement have equal scope and hence are children of the root. Stepping
into the code block following the input
statement, the code blocks following result == "jupiter"
and _
have equal scope; as such they are both children
of the input
statement. Finally looking at the code block following _
, both act
statements and the hop statement have equal scope, and hence are all children of the _
statement. With this in mind the hop statement is essentially saying, return to the third ancestor of
this code block.
Up to now all program behavior has been predefined. Variables have been assigned persistent values, and all output can be predetermined ahead of time. However part of what makes any program engaging and meaningful, is when a user or programmer is able to directly interact with the application. In this section we showcase how DMS captures and processes user input. In DMS, user input is handled by a special structure which assigns intents to a temporary user-defined variable, as follows.
once { act "Hi there! I'm Parrot-Bot. I repeat anything you say!" } input -> result { result == "Hello" { act "Ahoj to you!" } _ { act "You said: " + result } }
In order to begin capturing input, we use the syntax input ->
.
The expression can be thought of as follows,
“Anything that is typed in or captured,
redirect and store in the variable immediately following the arrow ->
“.
In the example above, all captured input is stored in the variable result.
Once DMS is done capturing input, the body of the input block begins its execution.
Simply put, the conditions contained within the body of the input block can be thought
of as if-else expression.
This means that the first expression, using the input, that evaluates to true will be executed.
In the above example, whenever user input happens to be "Hello" the program will output "Ahoj to you!",
(Note: Ahoj is Czech for Hello) and in all other cases will default to the expression beginning with the underscore _
.
Note that within an input block whenever an underscore _
is used as the condition of an expression,
it will always evaluate to true; essentially acting as the default else of a branching statement.
Most programming languages allow programmers to use external packages/libraries and launch child processes from anywhere within the main process. In this chapter we demonstrate how programmers are able to import and run components which can be thought of as scripts from within the main DMS program. Being able to import and launch components allows for programmers to develop modular and sophisticated DMS programs.
The use
statement allows programmers to import variables and user-defined
functions from an external component or script.
Consider the following example where we have a component containing a number of math functions,
and a main component wanting to reuse them.
Create the following two files: main.dms
and math.dms
.
Inside math.dms
place the following code segment.
once { pi = 3.14 def square(number) { pop number * number } def increment(number) { pop number + 1 } }
Now suppose that we wanted to use both the square
and increment
functions.
Inside the main.dms
file we write the following,
once { use "math" import _ } act square(2) act increment(3) act pi
In order to introduce the component, we begin with the keyword use
,
followed by the component name, in this case math
.
Note that we leave off the file extension.
Next in order to bring all predefined functions into scope within
main.dms
we use the keyword import _
.
This can essentially be thought of as bringing everything that was
defined within main.dms
into scope and made usable.
The manner in which components are introduced might be a little tough to understand.
At a high level the use of components can be thought of as follows.
Suppose we're making curry. We have a recipe, which calls for spices.
Naturally we ask ourself, which ones? To which our recipe might say, all of them.
And so we proceed to bring all the individual ingredients defined in spices
and use them within our curry recipe.
In the last section we introduced the idea of a component or simply a script.
Components not only make a programmers life easier by allowing DMS code to be modular,
but also save developers from writing their entire code base within a single component.
DMS allows programmers to launch sub-components from within a main-component.
At a high level this means that if our current task is to make dinner,
completing the sub-task of washing dishes gives us the clean dishes we need to finish our main task.
Consider the following DMS code which does exactly that. Create the following two files: main.dms
and wash_dishes.dms
. Inside wash_dishes.dms
place the following code segment.
once { num_dishes = 1 } if num_dishes >= 2 { act "done" pop true } num_dishes = num_dishes + 1
Similarly within main.dms
place the following.
once { clean_dishes = false } act "Lets make some food!" if !clean_dishes { run "wash_dishes" () -> result { result { act "Ok lets get to cooking!" } _ { act "On second thought, lets order in." } } }
There’s a lot going on in the above example,
so lets break it down. In order to launch a sub-component we use the keyword run
.
The line containing run "wash_dishes" () -> result
tells DMS that we want to pause our current component,
run the sub-component, and wait for it to finish.
Upon finishing, the wash_dishes
sub-component returns a Boolean value denoting its success,
which is stored in result.
(Note that the returned value of the sub-component does not have to be a Boolean value,
and can actually be any of the atomic or structure data types defined in the previous chapter.)
The expressions that follow are again an example of conditional if-else
expressions.
The first expression that evaluates to true will be executed, and the others will be ignored.
In this case because we happen to be returning a Boolean value, whenever the result is true
,
the first expression will be executed, and our program defaults to the case
beginning with an underscore _
as this evaluates to true
.
In the run example we showed how to launch a sub-component by calling, run "wash_dishes" () -> result
from within the main component. Note the empty parentheses, ()
, following the name of our sub-component. In DMS
we can pass arguments to our sub-component by placing all intended parameters (i.e. arguments) within ()
.
The sub-component can then access these arguments by unpacking or indexing the builtin _args variable.
In the following example we show how to pass arguments to our sub-component and access their values using both
array indexing and unpacking notation.
Create two files: jury.dms
and decision.dms
. Within jury.dms
place the
following code snippet.
once { first = "Nishant" last = "Shukla" run "decision" (first, last) -> verdict { verdict { act "Jail forever." } _ { act "Freedom!" } } }
Similarly, within decision.dms
place the following code snippet. Here we show how to unpack the
_args builtin variable using both aforementioned notations.
// unpack the array first, last = _args // or, access the array by index first = _args[0] last = _args[1] pop first == "Nishant" && last == "Shukla"
Note: The _args builtin variable is null in any component that is not called by another
component, such as a main
component, and is otherwise an empty list.
Beyond this point we begin to define the key ideas that make DMS special. We introduce the notion of what makes DMS a task-oriented programming language and illustrate how tasks can be broken down into actions that have a corresponding utility. With this notion of tasks, actions, and utility we show how DMS allows programmers to rid themselves of the overhead of having to think about the sequential-steps required to achieve a task and simply let DMS pick the best sequence of actions for us. Lets dive in!
The fork
statement is the most important aspect of DMS.
It lists possible branches of program flow in a declarative manner.
In this section, we'll design a chat-bot that asks "What's the capital of France?". If we type an incorrect answer, such as "Rome", then the script will diagnose the mistake, and help us get to the answer.
First, set up a couple variables and constants.
A score
variable will be used to track dialogue quality.
The entities, such as ROME
, and relations, such as CITY_IN
,
will be declared as variables, just to keep our code clean.
We'll also track a user_response
variable that indicates whether
the dialogue has already processed the user's input.
once { score = 0 EUROPE, FRANCE, ITALY, ROME, PARIS, LYON = ["Europe", "France", "Italy", "Rome", "Paris", "Lyon"] COUNTRY_IN, NEXT_TO, CITY_IN, CAPITAL_OF = ["a country in", "next to", "a city in", "the capital of"] user_response = null }
else
branch until
the user_response
variable is reset to null
.
if user_response == null { // get the user's input } else { // interpret the user's input }
The complete script is shown below.
once { score = 0 EUROPE, FRANCE, ITALY, ROME, PARIS, LYON = ["Europe", "France", "Italy", "Rome", "Paris", "Lyon"] COUNTRY_IN, NEXT_TO, CITY_IN, CAPITAL_OF = ["a country in", "next to", "a city in", "the capital of"] user_response = null } if user_response == null { answer = "" relation, entity = [CAPITAL_OF, FRANCE] act "What is " + relation + " " + entity + ": " + "Italy, Rome, Paris, or Lyon?" input -> response { _ { user_response = response } } } else { #{depth: 4, model: [[{score: 10}, {score: 0}]]} fork { [relation, entity] == [COUNTRY_IN, EUROPE] { answer = ITALY } [relation, entity] == [NEXT_TO, FRANCE] { answer = ITALY } [relation, entity] == [COUNTRY_IN, EUROPE] { answer = FRANCE } [relation, entity] == [NEXT_TO, ITALY] { answer = FRANCE } [relation, entity] == [CAPITAL_OF, ITALY] { answer = ROME } [relation, entity] == [CITY_IN, ITALY] { answer = ROME } [relation, entity] == [CITY_IN, FRANCE] { answer = LYON } [relation, entity] == [CAPITAL_OF, FRANCE] { answer = PARIS } [relation, entity] == [CITY_IN, FRANCE] { answer = PARIS } relation == CAPITAL_OF { relation = CITY_IN act "Not every city is the captial" } relation == CITY_IN { relation = CAPITAL_OF } relation == CAPITAL_OF { relation = NEXT_TO } relation == NEXT_TO { relation = CAPITAL_OF } entity == FRANCE { entity = ITALY act "I think you're thinking of a different country" } entity == ITALY { entity = FRANCE act "I think you're thinking of a different country" } answer == user_response { user_response = null act answer + " is " + relation + " " + entity score = score + 1 } } }