Art of Conversation: Explicit Conversation – Part 3

In my previous blog entry, I talked about implicit conversations. A quick recap: implicit conversations are conversations that are created for you by the Vorpal framework whenever you send or receive an IQ get or set. These conversation are terminated when you send or receive an IQ result or error.

Conversation gives you the ability to store states either in a ConversationContext object or in the message handler class annotated with @ConversationScoped. For example, you can associate some state with an outgoing IQ get packet; so when the reply returns, you can look at the state know what to do with it. When Vorpal creates another implicit conversation in the context of an ongoing conversation, the newly created conversation will inherit the states from the ongoing conversation. This is known as conversation inheritance.

In this blog, we will introduce explicit conversation, viz conversation that are directly under the control of the application; explicit conversation share lots of similar properties as implicit conversations but build on these concepts.

What are explicit conversations?

In XMPP, conversation between 2 Jabber entities are marked with <thread>; see this document. You will typically find <thread> element in message. This allows both parties to track the packets between the 2 conversing entities. Below is an example of a pair of message exchange between a patient and his psychiatrist.

<message from=”normanb@arkham” to=”eliza@arkham” type=”chat”>
<body>I want my mother</body>
<thread>1234567890</thread>
</message>

<message from=”eliza@arkham” to=”normanb@arkham” type=”chat”>
<body>Why do you need your mother?</body>
<thread>1234567890</thread>
</message>

To see how to use explicit conversation, lets write s service, call Eliza (which is a bot), that provides psychiatric help.

@ConversationScoped
@Message
@To(“eliza@{__subdomain__}”)
public class ElizaService {
   @Inject @Named(“__conversation__”) Conversation conv;
   private Eliza eliza = null;

   @PostConstruct private void init() {
      conv.begin();
      //Initialize Eliza
  
   eliza = new Eliza();
        
   }

   @Body(“{body}”)
   private String handleMessage(@Named(“body”) String body) {
      if (“bye”.equals(body))
         conv.end();
      return (eliza.process(body));
   }
}

Assume we receive a new message with a thread id of 1234567890; also assume that this is the first time that Eliza service is seeing this new thread id.

So what we want to do is associate an instance of Eliza object with this thread. We will continue to use the same Eliza instance for messages with the same thread id until the sender decides to terminate the conversation.

To do that we annotate the message hander with @com.kenai.jabberwocky.framework.ConversationScoped. Do note that the @ConversationScoped is from com.kenai.jabberwocky.framework package and not from javax.enterprise.context. Any class that is annotated with this will be associated with the same conversation for the life time of that conversation.

We also inject an instance of Conversation object into the handler. The Conversation object is use to start and terminate a conversation. For explicit conversation, if you do not explicitly start them, the conversation will not begin. It is also important that we qualify the conversation with @Named(“__conversation__”) or else the appserver will try to inject a Conversation instance from JSF instead of from Vorpal.

This is how the code works: after the handler has been instantiated; the @PostConstruct method will start the conversation by calling conv.begin(). Since ElizaService class is annotated with @ConversationScoped, this particular instance will be associated with this thread id for the life time of the conversation.

After invoking @PostConstruct, normal method processing occurs viz. handleMessage() will be called. When we receive an ‘bye‘ in the message body, we terminate the conversation by calling conv.end(). After that the ElizaService instance will be discarded. A new instance will be created even when the next message contains the same thread id.

Once you have started a conversation, any handlers annotated with @ConversationScoped that process messages from this open conversation will also be automatically associated with the conversation. This is known as a conversation group.

@ConversationScoped
@Message
@To(“eliza@{__subdomain__}”)
@DataForm
   public class Configure {

For example if the following is the one of the handler that is invoked after the patient has started normanb@arkham has started the conversation, then Configure object will be associated with the conversation. When the conversation terminates, all objects in the conversation group will be released.

Using ConversationContext

The above can be rewritten using ConversationContext. A CovnersationContext is a map and behaves like HttpSession. You can also use this to store the conversation state. The following shows ElizaService rewritten using ConversationContext

@Message
@To(“eliza@{__subdomain__}”)
public class ElizaService {
   @Inject @Named(“__conversation__”) Conversation conv;
   @Inject ConversationContext convCtx;

   @PostConstruct private void init() {
      conv.begin();
      Eliza eliza = new Eliza();
      convCtx.setAttribute(“elizaInstance”, eliza);
   }

@Body(“{body}”)
private String handleMessage(@Named(“body”) String body) {
   Eliza eliza = (Eliza)convCtx.getAttribute(“elizaInstance”);
   if (“bye”.equals(body))
      conv.end();
   return (eliza.process(body));
   }
}

You can use a combination of @ConversationScoped objects and ConversationContext to hold your conversation state.

How do you decide which to use? If you have a really complex object then @ConversationScoped objects are the way to go; but if you decide to share conversation states with all handlers (including those that are not annotated with @ConversationScoped) then you should consider ConversationContext.

Creating new conversation threads

Vorpal does not support nested conversations; furthermore a message handler can only deal with 1 conversation at a time. Let say we have the following use case; in the course of normanb@arkham‘s conversation with Eliza, Eliza would like to start a new conversation thread on a new topic with normanb. In other words, normanb is have 2 open conversation with Eliza (2 unique thread id). To do this, you need to manually insert a new thread id into an outgoing message packet. The following code shows how this is done

@Message
@To(“eliza@{__subdomain__}”)
public class ElizaService {
   @Inject @Named(“__conversation__”) Conversation conv;
   @Inject ConversationContext convCtx;

   @PostConstruct private void init() {
      conv.begin();
      Eliza eliza = new Eliza();
      convCtx.setAttribute(“elizaInstance”, eliza);
   }

   @Body(“{body}”)
   private ResponseContext handleMessage(@Named(“body”) String body) {
      Eliza eliza = (Eliza)convCtx.getAttribute(“elizaInstance”);
      ResponseContext reply = new ResponseContext(
      ResponseContext.Type.Message);

      if (“bye”.equals(body))
         conv.end();
      else if (body.toLowerCase().startsWith(“new topic”)) {
         body = body.substring(9).trim();
         //Insert a new ThreadID into the response
        
responseContext.add(ThreadID.generate());
      }           
      responseContext.add(eliza.process(body));
      return (responseContext);
   }
}

Assume that when the response from normanb starts with the phrase ‘new topic‘ then we want to start a new conversation thread. Under normal circumstances, if there are no open conversation, then what we do is inject an instance of Conversation object in, call begin(). The out going message will then be marked with a randomly generated thread id.

However in the situation above, we already are in the context of an open conversation; invoking begin() again will result in an IllegalStateException. In this situation, to let Vorpal know that the out going message is under a new conversation, you have to manually add the <thread> element into the message packet. You can either manipulate the Message packet or use ResponseContext as show above to add a thread id.

When Vorpal sends out the message, it’ll create a new a Conversation and a ConversationContext objects for this new conversation; it then
automatically starts the conversation for you viz. calls Conversation.begin(). This is the only time an explicit conversation behaves like an implicit conversation. You’ll still need to manually terminate it. The ConversationContext will inherit all the values from the existing open conversation.

This new way of creating conversation works in all handlers including IQ and Presence; for example, you can create a new conversation with another Jabber entity when you are handling a disco#info. The new conversation’s ConversationContext will inherit all the values from the ConversationContext of the currently opened implicit conversation.

The only restriction is that this works with messages (<message>) only.

Flash Conversations

Inheriting conversation only works when a new conversation is created and started in the context of an existing conversation; for example in the case of implicit conversation (eg. sending out disco#info get in a disco#item result handler), the out going IQ packet’s conversation is automatically started by Vorpal so it’ll inherit the conversation states of the existing conversation. Similar situation with inserting a new thread id into messages described above.

There are cases where you would like to inherit the conversation states from an existing conversation but you only wish to start the conversation in the next packet that you receive. Here is an example

  1. fred@myjabber sends a message to chess@playground.myjabber to create a chess game
  2. chess@playground.myjabber randomly generates a game room name abcde. Saves abcde@conference.myjabber and fred@myjabber in ConversationContext.
  3. sends a presence to abcde@conference.myjabber to create a chat room to host the game
  4. conference.myjabber returns after creating the room
  5. chess@playground.myjabber now sends a direct invitation to fred@myjabber. Also updates ConversationContext with any other data need for the game

Since the invitation to fred@myjabber is a message (<message>), no conversation will be started. What Vorpal does at this point is this; if there is an existing conversation, it’ll take all the states in the ConversationContext and save that in the flash conversation map. Vorpal constructs a pseudo thread id made up of the ‘to’ and ‘from’ attributes from the message.

Sometime in the future, when fred@myjabber confirms the chess game, Vorpal will now pickup the existing ConversationContext (and Conversation) object from flash and starts the conversation.

So flash conversations are conversations that are not started but have the potential to start sometime in the near future; its model after Ruby on Rails’ flash. If a flash conversation is not started within a certain time period (default is 3 mins), its discarded.

See Arkham in playground especially CreateChatroom.java and ConfirmChatroom.java.

You can find the latest bundle here.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: