Program structure
Context
Conversation context is a set of variables, that you may use during the conversation. You can define input, output and additional context variables.
context { // Input context variables input input_var1: number = 0; input input_var2: string | null; // Additional context variables context_var1: number = 0; context_var2: string | null // Output context variables output output_var1: number = 0; output output_var2: string | null; }
Node
A node describes the system's actions in the current state and the conditions for transitioning to other nodes.
Every node has its own <name>
, stated after the node
keyword.
Structure
Every node contains the following sections:
- do - a section describing actions performed in this node;
- transitions - a section describing transitions to subsequent nodes;
- onexit - an optional section describing the actions performed when exiting the node through certain transitions;
- ondigreturn - an optional section describing the actions to be performed when returning to the current node from a digression.
Example
node foo { do { /// a section describing actions performed in this node } transitions { /// a section describing transitions to subsequent nodes } onexit { /// an optional section describing the actions performed when exiting this node through specific transitions } }
Node's local data
Local data can only be accessed within the scope in which it is declared. Like context data, local data is statically typed.
Local data is not shared between executable sections of the node. For example, you cannot access the data declared in the do
section from the onexit
section of the same node.
Lifetime
At runtime, when entering an executable section (for example, do
section), an isolated scope containing local data is created. When the section's execution ends, the scope will be canceled, and its local data will be destroyed.
Example
start node myNode { do { // Declare local variable. var foo: number = 0; var bar = "Hello"; // Read from local variable. // Expected output: "Hello" #log(bar); } transitions { } }
Dialogue entry point
Every dialogue must contain a node marked with the start
keyword. This node will be an entry point of your dialogue, just like the main
function in other programming languages.
start node entry_point { do { } }
Transitions
Transitions describe conditions of choosing the next node. transitions
section of a node is just an enumeration of named transitions.
start node myNode { do { // Triggers waiting mode. wait *; } transitions { // Declaration of transition "transitionName" to node "nodeName" triggered by condition "myCondition". transitionName: goto nodeName on myCondition; } }
transitionName
specifies the name of the transition.nodeName
specifies target node.myCondition
specifies transition's condition.
You can omit the transitions
section entirely in nodes with no transitions (e. g., terminating nodes).
Transition on event
The most common way to trigger transitions is by waiting for an event. To decide which transition to trigger, an application checks the condition of every active transition in descending order of priorities until one of them is true
. You can activate transitions with wait
operator, flags and transition tags.
Here’s an example illustrating all the possible ways of declaring a transition on an event. All the described fields are optional. More often than not, you won’t need to use all of them in one transition:
node myNode { do { wait *; } transitions { tr: goto targetNode on #messageHasSentiment("positive") priority 100 tags: ontext; } }
on #messageHasSentiment("positive")
is a declaration of a condition (in this case transition is triggered with a positive sentiment);priority
(0 by default) allows to control the order of condition checks while handling an event: conditions are checked in descending priority order;tags: ontext
declares types of events triggering the transition (by default, tagontext
is used).
Timer transition
A timer transition can be declared using the on timeout <anyNumber>
keyword, where anyNumber specifies the timeout in milliseconds. The timer starts when the transition is activated by the wait statement.
node myNode { do { wait *; } transitions { timeoutTransition: goto targetNode on timeout 5; } }
Instant transition
An instant transition does not contain a condition description and is ignored when checking for events. Such transitions can be triggered with the goto
statement placed in the do
section (not to be confused with the goto
keyword in the transition declaration).
node myNode { do { goto instantTransition; } transitions { instantTransition: goto targetNode; } }
Library
The library is a plug-in code. During compilation, its content is copied to the place where the library was imported.
Library file description
A file is declared library using the library
directive at the beginning of the file. To include the library's content, use the import
keyword followed by the path to the linked library.
An example of declaring and importing a library file:
Library file myLib.dsl
library // A directive that declares the file to be a library. block myLibBlock(): boolean { start node root { do { #say("Hello from lib block!"); return true; } transitions { } } }
Main file
import "./myLib.dsl"; // Connecting the library file "myLib.dsl". start node hello { do { #connect("myEndpoint"); set $x = blockcall myLibBlock(); // Calling a block from a library file. exit; } transitions { } }
Block
The block is a graph analog of subroutines that provides a mechanism for code reuse. The main graph is implicitly a block too.
Block description
A block is declared using the block
keyword. A block can contain nodes, digressions and nested blocks.
block MyBlock(): void { start node root { do { // Custom code... } transitions { } } }
Returning from a block
To terminate the block and continue the graph's execution from the point the block was called at, use the return
keyword. You may also return some value.
Block call
Block call is performed with the blockcall
keyword, followed by the block's name and its parameters.
Example
start node foo { do { set x = blockcall bar("in", y: "put"); set $out = x; exit; } transitions { } } block bar(x: string, y: string): string { start node baz { do { return $x + $y; } transitions { } } }
Block context
Context is the isolated scope of the block. Block context is similar to main conversation context. The difference is that block context is isolated and main conversation context is not available inside the block. If you need to assign something to main conversation context return these values from block. To define block context use the context
directive inside the block.
The context can be accessed from any executable section and transitions inside the block using the $
prefix.
Example
start node root { do { blockcall MyBlock("Hello", "from"); exit; } transitions { } } // Declaration of block "MyBlock" with context `{ arg1: string, arg2: string }`. block MyBlock(arg1: string, arg2: string): boolean { // Context extension directive. context { // Add "myInputData" to context. myInputData: string = "MyBlock"; } start node root { do { // Expected output: "Hello" #log($arg1); // Expected output: "from" #log($arg2); // Expected output: "MyBlock" #log($myInputData); return true; } transitions { } } }
Async Block
Modificator async
denotes that this block is asynchronous.
Asynchronous block is the block which is executed asynchronously, i.e. in non-blocking way.
Execution of an asynchronous block can be thought as starting another dialogue at the same time (like an async thread).
Asynchronous block allow you to call #connect() and #connectSafe function (only once), i.e. to establish new single connection during the dialogue.
Only asynchronous blocks are allowed to call other asynchronous blocks.
Main graph context is technically an asynchronous block.
When created, the id
assigned to it. This id
can be used to route builtin functions controlling the behavior of channels established in async blocks.
When terminated, async block sends messages of type "Terminated"
to all other blocks (see Async Block Messages).
async block MyAsyncBlock(...args): returnType {}
Builtin functions to control async blocks behaviour:
Async Block Messages
To communicate with each other there is a mechanism of messages between async blocks.
type AsyncBlockMessage = { // notifies about bridging and undridging of async block channels biDirectional:boolean; messageType:"Unbridge"|"Bridge"; sourceRouteId:string; targetRouteId:string; }|{ // notifies that another async block terminating // (or is being terminated by) the current block messageType:"Terminate"|"Terminated"; sourceRouteId:string; targetRouteId:string; }|{ // notifies about sending content messages between async blocks content:unknown; messageType:"Content"; sourceRouteId:string; targetRouteId:string; }?
Digression
Digressions are a special type of nodes accessible from any other node in the graph despite not having any explicit transitions in them. Digressions allow a succinct description of reactions to context-independent events (e. g., questions like "what's your name") or events that may occur in several nodes.
After reacting to a digression, the graph may either proceed to the next node via ordinary transitions or return to the previous node with the return
keyword.
Examples
start node root { do { wait *; } transitions { } } // digression declaration digression myDigression { // digression's conditions conditions { on myCondition; } do { // return from the digression return; } transitions { } }
digression myDigression { condition on myCondition do { goto digressionContinuation; } transitions { digressionContinuation: goto digressionContinuation; } } node digressionContinuation { do {} transitions {} }
Preprocessor
A preprocessor is a special kind of digression that doesn't consume the message. Upon preprocessor's return, the wait
operator will continue processing transitions. Preprocessor digression can't have any transitions from it.
Preprocessor digressions are declared with the preprocessor
modifier.
Example:
// preprocessor declaration preprocessor digression myPreprocessor { conditions { on myCondition; } do { // return to the waiting node and continue event handling return; } }
Digression control
Digressions within a block are enabled by default, i.e. their conditions are checked when handling an event. digression disable { <digressionList> }
operator allows disabling digressions at runtime. Disabled digressions are ignored when handling an event. Digression could be enabled back via digression enable { <digressionList> }
operator.
You can use the digression save { <digressionList> }
operator to save states of several digressions in a variable of a special type — digression mask. It is handy when you need to alter the state of many digressions temporarily. Use the digression apply <digressionMask>
operator to restore the digressions' state from a saved mask.
You may omit curly braces when switching or saving the state of a single digression (e. g. digression disable whats_your_name
).
Examples
/** * insert declaration of "foo" and "bar" digressions */ start node myNode { do { // All digressions is enabled by default so "initialDigressionMask" // get mask with "foo" enabled and "bar" enabled. var initialDigressionMask = digression save { foo, bar }; digression disable { foo, bar }; // Now both digressions are disabled. digression enable foo; // Now "foo" is enabled and "bar" is disabled. digression apply initialDigressionMask; // Now both digressions are enabled. exit; } transitions { } }
Digressions activity status inheritance
If a child block contains a shared digression, its activity status will be inherited. However, if you change the shared digression's status in the child block, it won't be changed in the parent one.
Examples
File sharedDigression.dsl
library digression sharedDigression { conditions { on true; } do { #log("Hello from foo"); return; } transitions {} }
File main.dsl
import "sharedDigression.dsl"; start node root { do { digression disable sharedDigression; // Now digression "sharedDigression" is disabled. blockcall myBlock(); // Digression "sharedDigression" still disabled. exit; } transitions {} } block myBlock(): boolean { import "sharedDigression.dsl"; start node root { do { // Digression "sharedDigression" is disabled because of mask inheritance. digression enable sharedDigression; // Now digression "sharedDigression" is enabled. return true; } transitions {} } }
Digression parameters
Digressions can have some parameters to be read and changed from outside the digression itself. It may be useful for making reusable libraries.
Digression parameters are declared between the conditions
and do
section with the var
keyword:
digression myDigression { conditions { on myCondition; } var myDigressionData: string = "defaultValue"; do {} transitions {} }
Shared digressions inherit parameter values. Keep in mind that parameters changed in the child block don't do so in the parent one unless declared with the shared
modifier.
Accessing digression parameters
Digression's parameters are accessible from any executable section and condition in the block. You can use them in the same way you use variables. Use the format digression.digression_name.parameter_name
to access the parameters:
/** * insert "targetNode" declaration here */ node myNode { do { // expected output: "defaultValue" #log(digression.myDigression.myDigressionData); // modify digression parameter set digression.myDigression.myDigressionData = "customValue"; // expected output: "customValue" #log(digression.myDigression.myDigressionData); wait *; } transitions {} }
Digression parameter inheritance
Different blocks may contain digressions of the same name; such digressions are considered the same. If the block contains an instance of the same digression as its parent (or any other predecessor), this digression inherits its parent's parameters' values. Such digressions are called shared.
Example of parameter value inheritance
File sharedDigression.dsl
library // This digression will be imported in the root block and in "myBlock", // so it's considered shared between them. digression sharedDigression { conditions { on #messageHasSentiment("positive"); } // Initialize value in the scope of the root block. var data: string = "defaultValue"; do { return; } transitions {} }
File main.dsl
import "sharedDigression.dsl"; start node root { do { // Expected output: "defaultValue" #log(digression.sharedDigression.data); set digression.sharedDigression.data = "newValue"; // Block "myBlock" will inherit `digression.sharedDigression.data` value. set tmp = blockcall myBlock(); exit; } transitions {} } block myBlock(): boolean { // Because an instance of "sharedDigression" exist in the calling block, // "myBlock" will inherit its data. import "sharedDigression.dsl"; start node root { do { // Expected output: "newValue" #log(digression.sharedDigression.data); return true; } transitions {} } }
Parameters are copied on inheritance. So, changing the parameter's value in the child block does not affect its value in the parent block:
The peculiarity of copy inheritance of parameter values
File sharedDigression.dsl
library // This digression will be imported to root block and to "myBlock", // so it considered shared between them. digression sharedDigression { conditions { on #messageHasSentiment("positive"); } // Initialize value in the scope of the root block. var data: string = "defaultValue"; do { return; } transitions {} }
Файл main.dsl
import "sharedDigression.dsl"; start node root { do { // Expected output: "defaultValue" #log(digression.sharedDigression.data); blockcall myBlock(); // Expected output: "defaultValue" #log(digression.sharedDigression.data); exit; } transitions {} } block myBlock(): boolean { import "sharedDigression.dsl"; start node root { do { // Modify shared digression parameter in child block. set digression.sharedDigression.data = "newValue"; // Expected output: "newValue" #log(digression.sharedDigression.data); return true; } transitions {} } }
Shared parameters
Sometimes it's necessary to share a parameter's value between several digression instances. You can do this by declaring the parameter as shared
.
An example of declaring and using a shared parameter
File sharedParameter.dsl
library // This digression will be imported to root block and to "myBlock", // so it considered shared between them. digression sharedDigression { conditions { on #messageHasSentiment("positive"); } // Initialize value in the scope of the root block. shared var data: string = "defaultValue"; do { return; } transitions {} }
File main.dsl
import "sharedParameter.dsl"; start node root { do { // Expected output: "defaultValue" #log(digression.sharedDigression.data); blockcall myBlock(); // Expected output: "newValue" #log(digression.sharedDigression.data); wait *; } transitions {} } block myBlock(): boolean { import "sharedParameter.dsl"; start node root { do { // Modify shared digression parameter in child block. set digression.sharedDigression.data = "newValue"; // Expected output: "newValue" #log(digression.sharedDigression.data); return true; } transitions {} } }
Immediate reaction
Using when
constructions, you can handle immediate reaction on converstation event.
when <event> [yourName] [priority number] do { ...some code }
;
Where event
is required and can have following values:
starting
- executed beforestart
nodeexiting
- executed just afterexit
instruction (orreturn
inside block)opened
- executed after a successful call of#connect
or a#connectSafe
callblockMessage
- executed when anyasync block
sends a message to this one
Each reaction will be executed in an order sorted by descending priority (Firstly with priority N, then with priority N-1 etc.)
Each when
section can call an exit
, except when exiting
.
Exit reaction
You can write multiple when exiting do { ...some code }
for handling end of the conversation and do some post-processing,(e.g. analyze a conversation or call your backend).
If an exit occurs in the block, then all exiting
whens will be called in the current block, and then in the parent block, until stack will unwind to the empty one.
Block return someValue;
will call all exiting
whens of this block (and only this block).
Opened reaction
You can easily limit the duration of the dialog just adding following lines
when opened do { set digression.duration_limiter.conversationStartTime = #unixTimeStamp(); } preprocessor digression duration_limiter { conditions { on #unixTimeStamp() > digression.duration_limiter.conversationStartTime + digression.duration_limiter.maxDurationSeconds*1000 tags: ontick; } var maxDurationSeconds: number = 60*60; var conversationStartTime: number = 0; do { #log("Max conversation duration exceed"); exit; } }