Galaxy Communicator Tutorial:

Writing a Basic Hub Program File

License / Documentation home / Help and feedback

In our hub-and-spoke configuration, we've seen that it's the Hub which maintains connections to all the servers and routes the message traffic. We've already learned a great deal about how Hub program files work, even though we haven't looked yet at the technical details. In this lesson, we'll learn the basics of how program files are constructed. We'll learn about other Hub program functionality in future lessons.



A little bit about Hub program file syntax

A line in a Hub program file can be If the final character of a physical line in a directive entry is a backslash (\), the line break is ignored and the next line is treated as part of the same directive entry.

There are three types of information in the program file:

There are lots of directives, and each directive expects a value of a certain type or format. We're only going to look at a few of the directives and values. You can consult the complete documentation if you want to know more.


Global information and server declarations

As a first example, we'll begin with the program file we used in the second unit tester exercise. In that exercise, you'll recall, there were two servers: the Parser server from the toy travel demo, and the unit tester, acting as a server. The Hub contacted the Parser server, while the unit tester contacted the Hub. We discussed this process in our lesson on how the Hub and servers communicate. Here's the relevant fragment of the program file parse.pgm:
[parse.pgm]

 1:;; Use extended syntax (new in version 3.0).
 2:
 3:PGM_SYNTAX: extended
 4:
 5:SERVICE_TYPE: UI
 6:CLIENT_PORT: 14500
 7:
 8:SERVER: Parser
 8:HOST: localhost
10:PORT: 10000
11:OPERATIONS: Parse

Let's look first at the syntax of this fragment. Now that we know what we're looking at, let's take a closer look at the directive entries.

PGM_SYNTAX:

In version 3.0 of the Galaxy Communicator infrastructure, we extended the syntax of the Hub program file and made it more consistent overall. However, since these modifications were not backward compatible, we chose to enable them through an explicit directive entry. Always begin your program file with this entry.

Atomic string values

The value of the PGM_SYNTAX: directive, as well as a number of the other directives we'll discuss here, is a string. Typically, values for directives have the same printed form as they do in frames. So we'd expect line 3 to look like this:
PGM_SYNTAX: "extended"
However, for readability purposes, the Hub program file parser also recognizes these values (and sequences of these values) without their delimiting quotation marks. This is the last interpretation considered; so, for instance, a directive value which is a number will be interpreted as such, rather than a string consisting entirely of digit characters.

Service types and service providers

You may recall from our lesson on the toy travel demo that the Hub distinguishes between service types and service providers. The idea is that the Hub might want or need access to multiple servers of the same conceptual type: multiple recognizers for load balancing, multiple audio servers for multiple simultaneous users. The conceptual type corresponds to the service type. Service types have a name and a set of operations; the actual servers which implement these operations are the service providers.

Each of these is defined by a separate block, terminated by a blank line (or the first directive entry of another block of the same type). The two directive entry blocks in this Hub program file fragment illustrate two dimensions of this distinction.

SERVICE_TYPE:

Each service type has a name, which is the value of the SERVICE_TYPE: directive. A block which begins with the SERVICE_TYPE: directive may also contain an OPERATIONS: directive and a CLIENT_PORT: directive, among others. The value of this directive is a string.

CLIENT_PORT:

A service type may set up a listener to await connections from providers for this service type. The CLIENT_PORT: directive is a number which specifies the port to set up the listener on. So in this fragment, the UI service type will set up a listener on port 14500.

OPERATIONS:

Each service type may declare a set of operations, which are dispatch functions it expects the providers to implement. The Hub will choose a service provider to use in a rule only if the Hub program file has declared that the corresponding service type supports the operation, via the OPERATIONS: directive. The value of this directive is a sequence of strings. In this fragment, the UI service type declares no operations (so the only messages it will receive from the Hub are replies to messages it sends).

SERVICE_PROVIDER:

Each service provider block corresponds to a service provider the Hub will contact. The provider implements a service type (sometimes more than one); the value of the SERVICE_PROVIDER: directive is the name of the service type(s) it implements. This block may also contain a HOST: directive, a PORT: directive, and a LOCATION: directive, among others.

Since each service type defines a set of supported operations, all providers for a given service type must support the same operations (i.e., define the appropriately named dispatch function). So in the following example, both providers must define the Parse dispatch function:

SERVICE_TYPE: Parser
OPERATIONS: Parse

SERVICE_PROVIDER: Parser
HOST: localhost
PORT: 15000

SERVICE_PROVIDER: Parser
HOST: localhost
PORT: 16007

Multiple service types can define the same operation. So, for instance, you might choose to distinguish between parsers for French and parsers for Chinese by classifying them as different service types (there are better ways to do this, but let's assume it for the moment). They can both define the Parse operation, and any providers for these types must support that operation:
SERVICE_TYPE: ChineseParser
OPERATIONS: Parse

SERVICE_TYPE: FrenchParser
OPERATIONS: Parse

SERVER:

The SERVER: block is almost identical to the SERVICE_TYPE: block, except it may also contain the locations of various service providers for that type. In other words, lines 8-11 of this fragment are equivalent to
SERVICE_TYPE: Parser
OPERATIONS: Parse

SERVICE_PROVIDER: Parser
HOST: localhost
PORT: 10000

HOST:

Each service provider needs to specify the location of the listener that the Hub will contact. The value of the HOST: directive is a string naming the machine where the service provider's listener is.

PORT:

The value of the PORT: directive is an integer corresponding to the port number that the service provider's listener is listening on. So in this fragment, the Hub expects to find a service provider for the Parser service type listening on port 10000 on the local host.

LOCATION:

A shorthand for specifying HOST: and PORT: simultaneously. The value of this directive is a string <host>:<port>.

So this fragment declares two service types, UI and Parser. It tells the Hub to set up a listener for the UI service type, and to contact a service provider for the Parser service type at the local host, port 10000. Here's an illustration:


Hub program directives

For a quick introduction to programs in Hub program files, let's consider the program in parse.pgm:
PROGRAM: UserInput

RULE: :input_string --> Parser.Parse
IN: :input_string
OUT: :frame

In many ways, this is about a simple as programs ever get. It corresponds approximately to a small subsection of the toy travel demo.

PROGRAM:

You should remember from the basics lesson that when the Hub receives a new message, it tries to match the name of the incoming frame with the name of a Hub program. This directive entry begins a Hub program, and the value of the directive is a string which is the name of the program. Programs are terminated by the end of the Hub program file or by another PROGRAM: directive entry.

RULE:

Each Hub program is a sequence of rules. Each rule is a directive entry block terminated by a blank line. The RULE: directive entry indicates the beginning of a rule. This block may also contain IN: and OUT: directive entries, among others. The value of the RULE: directive is fairly complicated; it consists of a (possibly empty) set of conditions, an implication arrow (-->), and an operation name, which can be specified either as <operation> or <service_type>.<operation>.

The set of conditions is evaluated against the state of the current token. The condition here is a simple atomic condition requiring the presence of the :input_string key (with any value) in the frame comprising the token state. If this condition is satisfied, the Hub fires the rule, which means that the Hub finds a service provider for a service type which implements the named operation (in this case, Parse) and sends the provider an appropriate message. If the service type is specified in the operation name (in this case, Parser), only providers for the named service type will be considered.

IN:

When the Hub decides to fire a rule, it must construct a message to send. The name of the message will be the name of the operation (in this case, Parser.Parse). The value of the IN: directive describes how to construct the message given the current token. In this example, the instruction :input_string means that the Hub will look for a key-value pair in the token whose key is :input_string, and, if present, copy it into the message.

OUT:

When the server has finished executing the operation, it typically returns the result to the Hub. The value of the OUT: directive describes how to update the current token given the message return. In this example, the :frame instruction means that the Hub will look for a key-value pair in the message return whose key is :frame, and, if present, copy it into the current token.

Summary

Here's an illustration of the overall process:


Rules and namespaces

In order to understand the possible complexity of rules (and other program file directives that we won't be discussing here), it's important to understand the notion of namespace. We've seen that the Hub uses frames to maintain at least two distinct types of information states: tokens and messages. These types are interrelated: We'll refer to these types of information states as namespaces. In addition to the message and token namespaces, there are two others which we'll encounter: For more on namespaces, see the program file reference.

Each directive in a rule directive entry block has a namespace or namespaces associated with it. The RULE: directive is associated with the token namespace, for instance. The IN: and OUT: directives have both a source namespace (the memory state from which the pairs are drawn) and a target namespace (the memory state which is updated). The IN: directive's source namespace is the token namespace (that is, that's where the values come from), and its target namespace is the message namespace (that is, that's where the values go). For OUT:, it's the other way around.

For this discussion, let's assume a situation where our new UserInput message arrives at the Hub in a context where the global namespace contains the key-value pair :fragments_permitted 1 and the session namespace contains the key-value pair :language "English":

Specifying the namespace explicitly

It's possible to specify a namespace explicitly, using an operator named $in. You can use this operator to override the namespace defaults associated with a directive. So, for instance, the following two RULE: directive entries are equivalent, since the default namespace associated with the RULE: directive is the token namespace:
RULE: :input_string --> Parser.Parse

RULE: $in(:input_string token) --> Parser.Parse

If we want to fire a rule only if the global namespace contains a key-value pair whose key is :fragments_permitted, as in our example, we can write this rule as follows:
RULE: $in(:fragments_permitted global) --> Parser.Parse
So essentially, a frame key in a directive is a shorthand for a reference to that key in the default namespace.

Distinguishing between source and target namespaces

For those directives like IN: and OUT: which have both source and target namespaces, it's possible to distinguish between the source and target using a list of length two, where the first element is the target and the second element is the source. So the following two directives are equivalent:
IN: :input_string
IN: (:input_string :input_string)
In both cases, the directive looks for a key-value pair with the key :input_string in the source namespace and inserts the value in the target namespace under the key :input_string.

As you may have guessed at this point, you can use this syntax to refer to different keys in the source and target. So let's suppose that the Parse dispatch function expects the key :string, rather than :input_string, and returns the result in a key :parse, rather than :frame. If we wish to preserve the token state shown previously, we could rewrite our rule as follows:

RULE: :input_string --> Parser.Parse
IN: (:string :input_string)
OUT: (:frame :parse)
The value of :input_string in the source namespace for IN: (namely, the token) will be copied to :string in the target namespace (namely, the message), and the value of :parse in the source namespace for OUT: (namely, the message return) will be copied to :frame in the target namespace (namely, the token). Here's an illustration:

Using $in for source and target

Unsurprisingly, you can put these two things together, by overriding either the source or the target in the mapping list. Let's say we want to copy the value of the :language key in the session namespace to the :lang key in the message. The IN: directive entry would look like this:
IN: (:lang $in(:language session))
If the $in operator isn't part of a pair in IN: or OUT:, it's treated as the source key and namespace. The key is also used as the key for the target namespace, and the target namespace is the default. So the following directive entries are equivalent:
IN $in(:language session)
IN: (:language $in(:language session))
IN: ($in(:language message) $in(:language session))

Literal values

The pair notation for IN: and OUT: can also be used to specify literal values. These literal values appear in the source position. If, for example, we want to insert a literal confidence threshold in the message sent to the parser, we can do it as follows:
IN: (:threshold .8)
These literal values can be any frame key value, just like the values of directives, and the keys can be explicit namespace references.

Summary

This illustration unifies all these principles.


Rule conditions

So far, the only rule condition we've encountered is an existence condition, i.e., whether or not there's a key-value pair for the given key in the appropriate namespace:
RULE: :input_string --> Parser.Parse
The existence condition can be negated:
RULE: !:parse_completed --> Parser.Parse
The basic conditions also include numeric comparisons (>, <, =) and string equality (=) and their negations:
RULE: :threshold > .8 --> Parser.Parse
RULE: :input_string != "ignore" --> Parser.Parse
These conditions can be recursively combined using conjunction (&) and disjunction (|):
RULE: :input_string & (:threshold > .8) --> Parser.Parse
RULE: (:threshold > .8) | (:input_string != "ignore") --> Parser.Parse
The program file reference discusses rule conditions in considerably more detail.

Let's do a simple example to illustrate what happens when conditions don't match. Remember our simple program:

PROGRAM: UserInput

RULE: :input_string --> Parser.Parse
IN: :input_string
OUT: :frame

Now let's send a message that doesn't match.
[Hub program exercise 1]

Unix:

% process_monitor $GC_HOME/tutorial/program_file/nomatch.config

Windows:

c:\> python %PM_DIR%\process_monitor.py %GC_HOME%\tutorial\program_file\nomatch.config

Start the Parser, then the Hub, then finally the unit tester. (When you start the unit tester server, you won't be asked for a reply for the reinitialize message, as you were in the unit tester tutorial. This is because the unit tester is being started with the --ignore_reinitialize flag. See the unit tester reference for more details.) Select "Send new message", select the frame named UserInput, press "Reply Required" and then OK. You'll see the following in the Hub pane:
[Hub pane]

---------------- [ 1] ----------------------
{c UserInput
   :string "I WANT TO FLY FROM BOSTON TO LOS ANGELES"
   :session_id "Default"
   :tidx 1 }
--------------------------------------------

Done with token 1 --> returning to owner UI@<remote>:-1
Destroying token 1

And the unit tester interaction history will show that the result is almost identical to what was sent:
[Interaction History pane]

[Sending: new message]
{c UserInput
   :string "I WANT TO FLY FROM BOSTON TO LOS ANGELES" }
[Received: reply message]
{c UserInput
   :string "I WANT TO FLY FROM BOSTON TO LOS ANGELES"
   :session_id "Default" }

It should be clear what has happened. The UserInput program has a single rule, which fires if the token contains the :input_string key. The message we sent contains the :string key, but not the :input_string key, and so the token which is instantiated from the new message doesn't contain the :input_string key. As a result, the rule in the program doesn't fire, which means that the Parser server isn't invoked, and there are no updates to the token state. When the program ends (trivially, since it didn't fire the only rule), it returns the token state to the unit tester, since it asked for a reply.

Select "File --> Quit" to end this exercise.


Managing flow of control

Typically, the Hub evaluates a Hub program by considering each rule in turn. When a rule condition is satisfied, the Hub fires the rule and waits for the response from the server. When the Hub receives the response, it resumes the process at the rule immediately after the rule it fired, until it reaches the end of the program.

However, it's possible both to ignore the response from the server and to terminate the program before the final rule is considered. The simplest way to do this is through two special values of the OUT: directive, none! and destroy!.

The none! value

If the value of OUT: is none!, then the Hub will not wait for the server to respond, and will immediately move on to consider the next rule. If the server does respond, the response will be ignored. In essence, then, this rule and any subsequent rules which match will be fired simultaneously. We've already seen an example of this behavior.

You may recall from the toy travel demo that the input string was printed out by the IOMonitor server immediately before it was passed to the Parser server. The corresponding program file fragment looks like this:

PROGRAM: UserInput

RULE: :input_string --> IOMonitor.ReportIO
IN: (:utterance :input_string) (:who "user")
OUT: none!

RULE: :input_string --> Parser.Parse
IN: :input_string
OUT: :frame

By now, we can read the IN: directive entry for the first rule. The value for the :input_string key in the source (token) namespace is copied to the :utterance key in the target (message) namespace, and the :who key in the target namespace is given the literal string value "user". The value OUT: directive informs us that the response from the server will be ignored. So if the token has an :input_string key-value pair, both these rules will be fired at the same time.

We can see this behavior in action in the following exercise:

[Hub program exercise 2]

Unix:

% process_monitor $GC_HOME/tutorial/program_file/none.config

Windows:

C:\> python %PM_DIR%\process_monitor.py %GC_HOME%\tutorial\program_file\none.config

You'll see a process monitor with four panes. Start the Parser, IOMonitor, Hub and finally the unit tester server. Select "Send new message", and select the frame named UserInput, as before. Press "Reply required", and then OK.

Now take a look at the Hub pane. You should see something like this:

[Hub pane]

----------------[  1]----------------------
{c UserInput
   :input_string "I WANT TO FLY FROM BOSTON TO LOS ANGELES"
   :session_id "Default"
   :tidx 1 }
--------------------------------------------

Found operation for token 1: IOMonitor.ReportIO
Found operation for token 1: Parser.Parse
Serving message with token index 1 to provider for IOMonitor @ localhost:10050
---- Serve(IOMonitor@localhost:10050, token 1 op_name ReportIO)
Serving message with token index 1 to provider for Parser @ localhost:10000
---- Serve(Parser@localhost:10000, token 1 op_name Parse)
Got reply from provider for Parser @ localhost:10000 : token 1

----------------[  1]----------------------
{c UserInput
   :input_string "I WANT TO FLY FROM BOSTON TO LOS ANGELES"
   :session_id "Default"
   :tidx 1
   :frame {c flight ... } }
--------------------------------------------

Done with token 1 --> returning to owner UI@<remote>:-1
Destroying token 1

You can see that the two operations in the program matched essentially at the same time, and were sent to their respective servers at essentially the same time. The Parser server provided a response, and the token state is updated as before.

Select "File --> Quit" to end this exercise.

The destroy! value

The destroy! value also tells the Hub to ignore the response from the server, but it also tells the Hub to terminate executing the program. For instance, we might want to notify the IOMonitor and abort the execution of the UserInput program if the :input_string key was not present:
PROGRAM: UserInput

RULE: !:input_string --> IOMonitor.ReportIO
IN: (:utterance "<no input string found>") (:who "user")
OUT: destroy!

RULE: :input_string --> IOMonitor.ReportIO
IN: (:utterance :input_string) (:who "user")
OUT: none!

RULE: :input_string --> Parser.Parse
IN: :input_string
OUT: :frame

If there's no operation you want to fire, there are other ways of destroying the token; we'll talk about these when we talk about the Builtin server and building end-to-end systems.

The program file reference has more details about flow of control.


Summary

In this lesson, we learned about the following Hub program directives: Now that we understand the basics of writing both servers and Hub program files, we can move on to study functionality which involves both additional server capabilities and corresponding functionality in the Hub program file.

Next: Error handling


License / Documentation home / Help and feedback
Last updated August 8, 2002