Galaxy Communicator Tutorial:

Sending New Messages to the Hub

License / Documentation home / Help and feedback

Up to this point, we've studied only the simplest type of servers, which we exemplified using the Parser server. As we described in the server basics lesson, the Generator and Backend also are no more complex: all three servers accept messages and return a response. In this lesson, we're going to look at a slighly more complex server, one which doesn't return any responses, but rather sends new messages (and sometimes waits for their replies). This server is the Dialogue server.


An overview of the Dialogue server

The Dialogue server supports two dispatch functions. The first is DoGreeting, which is called when a user connects, and sends a greeting to the user. The second is DoDialogue, which decides what to do with each user input. In some cases, the Dialogue server will contact the Backend server to retrieve data from the database, and present the result to the user; in other cases, the Dialogue server might decide that it needs more information from the user before it continues. The full toy travel demo contains a small dialogue which exemplifies all three of these outputs:
# Greeting
System: Welcome to Communicator. How may I help you?
User: I WANT TO FLY TO LOS ANGELES
# Additional information
System: Where are you traveling from?
User: BOSTON
# Database query and response presentation
System: American Airlines flight 115 leaves at 11:44 AM, and United flight 436 leaves at 2:05 PM
The way the Dialogue server is implemented, all of these interactions involve sending a new message to the Hub. In addition, the database query involves sending a new message to the Hub and waiting for the response. We'll examine each of these elements in turn.


Asynchronous messages

The first message type we'll look at is asynchronous; that is, we don't ask for a response, and we don't wait for one. We'll choose the DoGreeting dispatch function to illustrate this.
Gal_Frame DoGreeting(Gal_Frame frame, void *server_data)
{
  char *s = "{c FromDialogue :output_frame {c greeting } :is_greeting 1 }";
  Gal_Frame greeting = Gal_ReadFrameFromString(s);

  GalSS_EnvWriteFrame((GalSS_Environment *) server_data, greeting, 0);
  Gal_FreeFrame(greeting);
  return (Gal_Frame) NULL;
}

The function we use to write new frames is GalSS_EnvWriteFrame. This function takes three arguments: a call environment, a frame to use as the message, and a flag indicating whether to wait for the frame to be written before returning. So when the Dialogue server receives a frame named DoGreeting, it constructs a frame named FromDialogue, writes it to the Hub, frees the frame, and returns NULL, which means that no frame will be returned (unless a reply is required, in which case a dummy will be sent). We can illustrate this with an example.
[New messages exercise 1]

Unix:

% process_monitor $GC_HOME/tutorial/messages/greeting.config

Windows:

C:\> python %PM_DIR%\process_monitor.py %GC_HOME%\tutorial\messages\greeting.config

In this example, the unit tester will pretend to be the Hub. Start the Dialogue server and then the unit tester. Select "Send new message", choose the DoGreeting frame, and press OK. The interaction history will show a new message:
[Interaction History pane]

[Sending: new message]
{c DoGreeting }
[Received: new message]
{c FromDialogue
   :output_frame {c greeting }
   :is_greeting 1 }

Note that we neither asked for a reply to the message we sent, nor did we receive one.

(If you're curious, you can repeat this exercise with "Reply required" selected, and you can see how the unit tester receives a dummy reply in addition to the new message.)

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


Synchronous messages

The process through which a server sends a message to the Hub and gets a reply is a little more complicated, although we've actually seen it happen a number of times; every time we've used the unit tester as a server and sent a message which requires a reply, we've done exactly the same thing that any other server does in the same circumstances. One such circumstance, as we've already described, is when the Dialogue server retrieves information from the database. The Dialogue server sends a new message named DBQuery, which invokes the corresponding Hub program:
PROGRAM: DBQuery

RULE: :sql_query --> Backend.Retrieve
IN: :sql_query
OUT: :column_names :nfound :values

When the program is done, the result is returned to the originating server. This interaction corresponds to the following small subsection of the toy travel demo:

From the unit tester

Before we look at the Dialogue server itself, let's try invoking this behavior from the unit tester.
[New messages exercise 2]

Unix:

% process_monitor $GC_HOME/tutorial/messages/backend.config

Windows:

C:\> python %PM_DIR%\process_monitor.py %GC_HOME%\tutorial\messages\backend.config

The unit tester will be functioning as a server in this example. Start the Backend server, the Hub, then the unit tester. Select "Send new message", select the first frame named DBQuery, press "Reply required" and then OK. You should see something like this in the unit tester interaction history:
[Interaction History pane]

[Sending: new message]
{c DBQuery
   :sql_query "select airline, flight_number, departure_datetime from flight_table where departure_aiport = 'BOS' and arrival_airport = 'LAX'" }
[Received: reply message]
{c DBQuery
   :nfound 2
   :values ( ( "AA"
               "115"
               "1144" )
             ( "UA"
               "436"
               "1405" ) )
   :sql_query "select airline, flight_number, departure_datetime from flight_table where departure_aiport = 'BOS' and arrival_airport = 'LAX'"
   :column_names ( "airline"
                   "flight_number"
                   "departure_datetime" )
   :session_id "Default" }

If you look at the Hub pane, you'll see that the new message matched the appropriate program, fired the rule to invoke the Backend server, and then terminated normally, returning the token state to the originating server (the unit tester):
[Hub pane]

----------------[  1]----------------------
{c DBQuery
   :sql_query "select airline, flight_number, departure_datetime from flight_table where departure_aiport = 'BOS' and arrival_airport = 'LAX'"
   :session_id "Default"
   :tidx 1 }
--------------------------------------------

Found operation for token 1: Backend.Retrieve
Serving message with token index 1 to provider for Backend @ localhost:13000
---- Serve(Backend@localhost:13000, token 1 op_name Retrieve)
Got reply from provider for Backend @ localhost:13000 : token 1

----------------[  1]----------------------
{c DBQuery
   :sql_query "select airline, flight_number, departure_datetime from flight_table where departure_aiport = 'BOS' and arrival_airport = 'LAX'"
   :session_id "Default"
   :tidx 1
   :column_names ( "airline"
                   ... )
   :nfound 2
   :values ( ( "AA"
               ... )
             ... ) }
--------------------------------------------

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

Formally, this interaction is identical to our previous interactions with the Parser server: the unit tester sends a new message to the Hub, the appropriate program is matched, the rules are fired and the token state updated, and the token state returned when the program ends.

If the Backend server raises an error, of course, the error will be forwarded to the originating server, as we saw in the last lesson. (Notice that there's no ERROR: directive in the Hub program rule, so the program won't continue.) We can see this again if we select "Send new message", select the second frame named DBQuery (the one with arrival_airport = 'SFO'), press "Reply required" and then OK. This time, the uniit tester interaction history will show an error reply:

[Interaction History pane]

[Sending: new message]
{c DBQuery
   :sql_query "select airline, flight_number, departure_datetime from flight_table where departure_aiport = 'BOS' and arrival_airport = 'SFO'" }
[Received: error message]
{c system_error
   :session_id "Default"
   :err_description "no DB result"
   :errno 0 }

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

From the Dialogue server

Now, let's take a look at the Dialogue server itself, to see how it does the same thing. This is a vastly simplified version of the actual dispatch function:
Gal_Frame DoDialogue(Gal_Frame f, void *server_data)
{
  Gal_Frame msg_frame = Gal_MakeFrame("DBQuery", GAL_CLAUSE);
  Gal_Frame r_f;
  GalIO_MsgType reply_type;
  char *sql_query;
  GalSS_Environment *env = (GalSS_Environment *) server_data;

  /* ... */

  Gal_SetProp(msg_frame, ":sql_query", Gal_StringObject(sql_query);
  response_frame = GalSS_EnvDispatchFrame(env, msg_frame, &reply_type);
  Gal_FreeFrame(msg_frame);
  switch (reply_type) {
  case GAL_ERROR_MSG_TYPE:
    /* Relay error */
    /* ... */
    break;
  case GAL_REPLY_MSG_TYPE:
    /* Construct presentation of database response */
    /* ... */
  }
}

Let's look at this a little more closely:

As before, we construct the message. This time, we use the function GalSS_EnvDispatchFrame, which takes three arguments: the call environment and new message, as before, and also a pointer to a message type variable. The message we receive will be either a normal reply (GAL_REPLY_MSG_TYPE) or an error reply (GAL_ERROR_MSG_TYPE), and the final argument captures this information. The frame which this function returns is the final token state of the Hub program invoked by the new message, or an error which the Hub is returning.

So when the Dialogue server receives a frame which causes it to consult the Backend server, this is how it gets the result from the Backend server via the Hub.

In this next example, the unit tester is going to act as a server. We're going to use the following program file:

;; Use extended syntax (new in version 3.0).

PGM_SYNTAX: extended

SERVICE_TYPE: UI
CLIENT_PORT: 14500
OPERATIONS: FromDialogue

SERVER: Dialogue
HOST: localhost
PORT: 18500
OPERATIONS: DoDialogue DoGreeting

SERVER: Backend
HOST: localhost
PORT: 13000
OPERATIONS: Retrieve

PROGRAM: DBQuery

RULE: :sql_query --> Backend.Retrieve
IN: :sql_query
OUT: :column_names :nfound :values

We're going to send a message named DoDialogue, and the Dialogue server will respond with a message named FromDialogue, as in the DoGreeting case. Now, neither of these messages corresponds to a Hub program by that name, but both of them correspond to operations, and the Hub will find the server with the appropriately named operation and send the message to it. The Dialogue server provides DoDialogue, and the UI server (!) provides FromDialogue.

This last bit is a little sneaky. The unit tester connects to the Hub, pretending to be the UI. The unit tester itself doesn't implement any dispatch functions, but it is constructed so it can field any message. So we're just relaying this message to that server so that you can see what the result of the interaction is. In effect, we're performing a slightly larger subset of the toy travel demo than just the interaction with the Backend; the unit tester is providing input to the Dialogue server, and digesting output from the Dialogue server:

Let's try it:
[New messages exercise 3]

Unix:

% process_monitor $GC_HOME/tutorial/messages/hub_backend.config

Windows:

C:\> python %PM_DIR%\process_monitor.py %GC_HOME%\tutorial\messages\hub_backend.config

Start the Backend, Dialogue, Hub and then the unit tester. Select "Send new message", select the first frame named DoDialogue, and press OK. If you scroll through the output in the Hub pane, you'll see the following steps:
  1. Hub receives the DoDialogue message
  2. Hub dispatches the DoDialogue message to the Dialogue server, and destroys the token (because there was no program that matched, and no reply requested)
  3. Hub receives the DBQuery message
  4. Hub dispatches the DBQuery message to the Backend server
  5. Hub receives the reply from the Backend server, relays the result to the Dialogue server, and destroys the token (because the DBQuery program has ended)
  6. Hub receives the FromDialogue message from the Dialogue server
  7. Hub dispatches the FromDialogue message to the UI (unit tester), and destroys the token (because there was no program that matched, and no reply requested)
And the unit tester interaction history will reveal the result:
[Interaction History pane]

[Sending: new message]
{c DoDialogue
   :frame {c flight
             :origin "BOSTON"
             :destination "LOS ANGELES" } }
[Received: new message]
{c FromDialogue
   :tidx 3
   :output_frame {c db_result
                    :tuples ( ( "AA"
                                "115"
                                "1144" )
                              ( "UA"
                                "436"
                                "1405" ) )
                    :column_names ( "airline"
                                    "flight_number"
                                    "departure_datetime" ) }
   :session_id "Default" }

Now, let's look at how errors might be handled here. Remember, the DoDialogue dispatch function doesn't send a reply. This means that it can't send an error reply (well, it could, but since the Hub isn't waiting for a reply, it will discard whatever reply it gets). So the Dialogue server will have to send a new message when it gets an error from the Backend server. So select "Send new message", select the second frame named DoDialogue, and press OK. The Hub and servers will follow exactly the same steps, except that the Hub will receive an error reply from the Backend server, and the new message the unit tester receives will describe the error:
[Interaction History pane]

[Sending: new message]
{c DoDialogue
   :frame {c flight
             :origin "BOSTON"
             :destination "SAN FRANCISCO" } }
[Received: new message]
{c FromDialogue
   :tidx 6
   :output_frame {c error
                    :description "error consulting backend" }
   :session_id "Default" }

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


Putting it all together

At this point, we can add in the Generator and Parser servers and perform an even larger subsection of the toy travel demo: we can provide strings as input and see strings as output, as illustrated here:
We'll continue to expand our Hub program file, and we'll once again use the trick of assigning a phony operation (ReportIO) to the UI server (i.e., the unit tester). For simplicitly, we'll only show the Hub programs here. Note that some of these Hub programs have multiple rules:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;            MAIN INPUT BODY
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

PROGRAM: UserInput

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

RULE: :frame --> Dialogue.DoDialogue
IN: :frame
OUT: none!

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;            MAIN OUTPUT BODY
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

PROGRAM: FromDialogue

RULE: :output_frame --> Generator.Generate
IN: :output_frame
OUT: :output_string

RULE: :output_string --> UI.ReportIO
IN: :output_string
OUT: none!

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;            DB SUBQUERY
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

PROGRAM: DBQuery

RULE: :sql_query --> Backend.Retrieve
IN: :sql_query
OUT: :column_names :nfound :values

Let's do one more exercise:
[New messages exercise 4]

Unix:

% process_monitor $GC_HOME/tutorial/messages/string_io.config

Windows:

C:\> python %PM_DIR%\process_monitor.py %GC_HOME%\tutorial\messages\string_io.config

The two available inputs in this exercise correspond to the two inputs in exercise 3. There are a number of processes in this exercise; you should be able to start them all by selecting "Process Control --> Restart all". When the unit tester window appears, select "Send new message", select the first frame named UserInput, and press OK. You'll see the following interaction in the interaction history:
[Interaction History pane]

[Sending: new message]
{c UserInput
   :input_string "I WANT TO FLY FROM BOSTON TO LOS ANGELES" }
[Received: new message]
{c UI.ReportIO
   :session_id "Default"
   :output_string "American Airlines flight 115 leaves at 11:44 AM, and United flight 436 leaves at 2:05 PM" }

If you like, you can select the Hub pane in the process monitor and trace the history of this interaction; at this point in the tutorial, you should be able to do that without assistance.

To induce the error, select "Send new message" in the unit tester, select the second frame named UserInput, and press OK, and then look at the interaction history:

[Interaction History pane]

[Sending: new message]
{c UserInput
   :input_string "I WANT TO FLY FROM BOSTON TO SAN FRANCISCO" }
[Received: new message]
{c UI.ReportIO
   :session_id "Default"
   :output_string "I'm sorry, but I can't get your answer from the database" }

Again, inspect the history of the interaction in the Hub pane if you like.

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


Summary

In this lesson, you've learned about the following concepts and components: At this point, we've learned enough about servers and program files to understand a substantial subsection of the toy travel demo. In the next lesson, we'll take a look at the next level of complexity: backchannel audio connections, known as "brokers".

Next: Setting up a brokered audio connection


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