1

Reference Guide

Table of contents
  1. Reference Guide
  2. Choice of Chat Platforms
  3. Basic Workflow
    1. 0. (Startup)
    2. 1. Events are received from the chat platform.
    3. 2. These are mapped to the Action class hierarchy.
    4. 3. Actions are processed by ActionConsumers
      1. CURRENT_ACTION
    5. 4. Responses are created
    6. 5. Responses are sent to ResponseHandlers.
  4. Annotations
    1. @Work
    2. @ChatRequest
      1. Simple Commands
      2. Multiple Commands
      3. Pattern Matching
      4. Restricting Availablity
    3. @ChatButton
      1. Restricting Availablity
    4. @Template
    5. @Display
    6. @Dropdown
    7. @ChatResponseBody
  5. Parameter Resolution
    1. 1. @ChatVariable Annotated Parameters
    2. 2. Addressable, Chat, User
    3. Spring Beans
    4. Form Data
    5. Other Message Parts
    6. Historical Messages
  6. Returning From Controller Methods
    1. Response Handlers
  7. Content Classes
    1. Features of Content Objects
      1. Simple Content
      2. OrderedContent
  8. Conversations API
  9. History
  10. Addressing Checkers
  11. Templating
    1. Templating Content Objects in a MessageResponse
    2. Templating @Work Objects in a WorkResponse
    3. Naming Templates
    4. Customizing Templates
    5. Validation
    6. Supported Property Types
    7. Extending Support To New Types
    8. Limitations
  12. Help Controller

Choice of Chat Platforms

Basic Workflow

This diagram shows the process for dealing with events on the Symphony platform, although other platforms work in the same way.

Workflow Events from/to the Symphony Platform

0. (Startup)

When your application starts up, Spring does a class-path scan of your project to find your beans. As part of this process, a ChatHandlerMapping instance is created for each controller method annotated with @ChatRequest or @ChatButton (see Annotations).

This is very much like Spring Web MVC, which tries to map controller methods to HTTP-endpoints by the @GetRequest annotation (and others).

1. Events are received from the chat platform.

These will be dependent on the platform itself, but usually will be things like:

…etc.

2. These are mapped to the Action class hierarchy.

Actions are a platform-agnostic view of upstream events. The table below summarises:

Class Features
Action - getAddressable(): the source of the action, i.e. the room or chat it originated in.
  - getUser(): the user that originated the action.
  - getData(): data associated with the action (usually a Map<>)
FormAction extends Action, but also has getFormData() for the data posted in the form. Used when a form is posted to the bot. Form data will generally be an @Work-annotated POJO, although if the form data can’t be mapped to a POJO, then the FormSubmission class is used instead which captures the form data in a simple map.
SimpleMessageAction extends Action, but contains getMessage(), for the message that was sent to the bot. Used when a user or other bot chats with your bot.

3. Actions are processed by ActionConsumers

Spring Bot bundles a number of ActionConsumer beans, which process the Actions. All ActionConsumer beans receive all actions. You are free to define your own ActionConsumer beans in your application.

Of special interest is the ChatHandlerMappingActionConsumer. This is responsible for routing Actions to the ChatHandlerMapping instances, which in turn mediate between the Action and calling your controller method.

CURRENT_ACTION

Action has a thread-local field called CURRENT_ACTION which you can call to get the details of the action being processed by the current thread, at any time. This is useful for initializing values, for example here we are initializing the Reminder object so that the author is the user from the action:

@Work
public class Reminder {
  
  String description;
  
  @Display(name = "Remind At")
  LocalDateTime localTime;

  User author = Action.CURRENT_ACTION.get().getUser();

  // getters / setters

}

4. Responses are created

Response classes have different purposes as follows:

Class Purpose
Response All responses have getAddressable() for where they are to be sent
- DataResponse All responses sending data to a chat will implement this class. Has getTemplate(), which dictates the format of the message, and getData() to indicate the content of the data-part of the message.
MessageResponse A particular type of DataResponse, which contains a Message body (see Content Classes)
AttachmentResponse Like a MessageResponse, but also contains an attachment to include with the message
ErrorResponse You can wrap a Throwable/Exception in this and send it as a response when your process fails (useful for debugging).
WorkResponse Contains a @Work-annotated POJO, which needs to be rendered and sent back, either as a form or display data

You can either return Response objects (or Lists of Response objects) from your controller method or ActionConsumer instance, or you can @Autowire the ResponseHandlers bean into your controller and use that directly.

5. Responses are sent to ResponseHandlers.

ResponseHandler beans are held in a priority-ordered list inside the ResponseHandlers bean, and responses pass along the chain of all ResponseHandlers. This means you can write ResponseHandler beans to “decorate” the Response object as it passes down the chain.

There are platform-specific ResponseHandlers which converts the generic Responses back into chat-platform specific API calls. So, messages for Symphony Addressables will be processed by the SymphonyResponseHandler, while teams or email Addressables can be processed by other ResponseHandlers.

Annotations

@Work

This annotation is designed to be put on any objects (POJOs, specifically) that will carry your model back and forth from your Spring runtime to the chat, and back again. For example, a ToDoItem will be annotated like this:


@Work
public class ToDoItem {

  // fields, getters, setters

By requiring this annotation, we achieve two things:

@ChatRequest

The @ChatRequest annotation on a controller method indicates that the method should be called when a user in conversation with the bot types some message that matches a given pattern.

Simple Commands

In it’s simplest form, this can be used on a Spring controller like so:


  @ChatRequest(value="new", description = "Create new item list")
  public ToDoList init() {
    return new ToDoList();
  }

In this case, when the user types /new then the method will be executed. In the case above, that means returning a TodoList object back to the user (which has the @Work annotation).

Multiple Commands

value can take multiple, different commands, allowing you to provide synonyms, or perhaps even alternate language versions of the pattern:


  @ChatRequest(value={"new", "nouveau"}, description = "Create new item list")
  public ToDoList init() {
    return new ToDoList();
  }

Pattern Matching

value can also match variables (given in curly braces {}), like so:

  @ChatRequest(value="delete {item}", description = "Remove items by number. e.g. \"/delete 5 6 7\"")
  public ToDoList delete(@ChatVariable(name = "item") List<Word> toDelete, Optional<ToDoList> toDo) {
    
     // body of method    

  }

Above, we are creating a chat variable called item, which will resolve to List<Word> (i.e. a list of words). The annotation @ChatVariable allows us to annotate a method parameter to receive this variable. Use:

Restricting Availablity

You can limit the command to just room administrators, or particular chat rooms.


  // particular rooms
  @ChatRequest(value="limited rooms", description="something only for certain rooms", rooms={"Special Room", "Admin Room"}})
  
 
  // only admins in a room
  @ChatRequest(value="only admin", description="Only room administrators", admin=true})
  

Beyond this, you can programmatically control whether or not the command does anything, as demonstrated by the RSS Bot.

@ChatButton

The @ChatButton annotation allows you to set up a button on a controller method. In the example below, we provide the button on the NewItemDetails form which allows the user to add a new element into their To-Do List.

  @ChatButton(value = NewItemDetails.class, buttonText = "add")
  public ToDoList add(NewItemDetails a, User u, Optional<ToDoList> toDo) {
    ToDoList out = toDo.orElse(new ToDoList());
    out.getItems().add(new ToDoItem(a.getDescription(), u, a.getAssignTo(), Status.OPEN));
    reNumber(out);
    return out;
  }

The @ChatButton annotationn requires the buttonText (i.e. the label) and the value (the class it appears on) to be set, as shown above.

Restricting Availablity

You can limit the button to just room administrators, or particular chat rooms.


  // particular rooms
  @ChatButton(value=SomeWork.class, buttonText="delete", rooms={"Special Room", "Admin Room"}})
  
 
  // only admins in a room
  @ChatButton(value=SomeWork.class, buttonText="delete", admin=true})
  

Beyond this, you can programmatically control whether or not the command does anything, as demonstrated by the RSS Bot.

@Template

The @Template annotation is used on a POJO class to indicate which templates it should use by default. Generally, all @Work objects can either be returned for viewing or editing (as a form). So template has two optional arguments view and edit where you can specify the name of a template to use for rendering the object in a given mode.

Here is an example from the Poll Bot, where we are specifying a template for the view mode:


@Work
@Template(view="answer")
public class Answer {

  // etc.
  

Note: @Template does not provide the path of the template. It is up to the ResponseHandler to resolve the template given it’s name. That’s because the template used on MS Teams will be an adaptive card template, whereas on Symphony it will be a Freemarker template. (See Response Handlers).

@Display

Display is used to guide the template-generation process. You can set the field name, or exclude the field from rendering in the template generator. Here is an example:

@Work
public class SubscribeRequest {
  
  @Display(name = "Subscription Name (Optional)")
  String name; 
  
  // more code

@Dropdown is a symphony-specific annotation also used to guide template generation. In the example of the code below, usually, a String class would be rendered as a simple input field (in edit mode). However, by using the @Dropdown annotation you can instruct the template generator to build a <select> picker, where the user can select from a limited set of options.

The data parameter indicates the source of the data within your Entity Json payload.

By default, this is expected to be a map, however you can set the format parameter for a custom data-structure. See RoomFormat for an example of that.

@Work public class TimezonePicker {

@Dropdown(data = “entity.timezones”) public String timezone;

}

@ChatResponseBody

This annotation is given as a clue to the template engine as to whether the response is expected in view or edit mode. For example, the method below returns a TodoList object, but the @ChatResponseBody tells us that this will be returned in edit mode (i.e. a form) rather than view mode (just for display).


  @ChatRequest(value="show", description = "Show current list of items")
  @ChatResponseBody(workMode = WorkMode.EDIT)
  public ToDoList show(Optional<ToDoList> in) {
    // method body
  }

You can also use @ChatResponseBody to specify a particular template to be used:


  @ChatResponseBody(template = "my-answer")
  public ToDoList showInSomeOtherWay() {
    // method body
  }

Parameter Resolution

You can specify controller methods annotated with @ChatButton or @ChatRequest with a variety of different parameters. These are resolved (i.e. bound to values) when your controller method is called by a number of built-in classes implementing the WorkflowResolverFactory interface.

1. @ChatVariable Annotated Parameters

If a method parameter is annotated with @ChatVariable (as below) then the ChatVariableWorkflowResolverFactory will resolve the parameter based on the message the user typed to the bot:

  @ChatRequest(value="delete {item}", description = "Remove items by number. e.g. \"/delete 5 6 7\"")
  public ToDoList delete(@ChatVariable(name = "item") List<Word> toDelete, Optional<ToDoList> toDo) {

2. Addressable, Chat, User

Add these parameters to a method to find out who the message came from, and the room or chat it came from. e.g.

  @ChatRequest(value="where")
  public Object details(User author, Optional<Chat> roomWhereItHappened) {
    // do something
  }
Class Purpose
Addressable An interface which indicates something that can be sent a message
User A sub-class of Addressable, indicating a specific user or a 1-1 chat.
Chat A multi-way chat, with multiple users, like a chat room or topic

Note: If you want a method to work whether it is happening inside a chat-room or not, you can use Optional<Chat> as the argument type, as shown above.

Spring Beans

You can add any spring beans as parameters to your mapped methods, although usually you will want to @Autowire them into the controller properties to keep your method signatures simple.

Form Data

User data can be captured in a form, based on a @Work-annotated POJO class. When this data is returned to your method, you will need a parameter to receive it, like so:

  @ChatButton(value = NewItemDetails.class, buttonText = "add")
  public ToDoList add(NewItemDetails a, User u, Optional<ToDoList> toDo) {
    // 'a' contains the form submission data from the NewItemDetails form
  }

Other Message Parts

Let’s say the user types a long message:

“Let’s go shopping on saturday, @Suresh Rupnar and @Robert Moffat”

We might not have a fixed pattern for this kind of message that we can put into @ChatRequest. However, we can capture the various parts of the message quite easily:

  @ChatRequest(value = "*")
  public void listen(Message wholeMessage, User firstuser, User secondUser) {
    // 'a' contains the form submission data from the NewItemDetails form
  }

todo: add another.

Historical Messages

For complex workflows, you might rely on some previous data from the chat room as context for whatever it is you are doing. For example, when we add an item to the To-Do list, we want to use the last known state of the To-Do list in the room, which might have been many messages ago.

  @ChatRequest(value="show", description = "Show current list of items")
  @ChatResponseBody(workMode = WorkMode.EDIT)
  public ToDoList show(Optional<ToDoList> in) {
     // parameter 'in' here contains the last To-Do list put in the chat where the /show command was typed.
  }

This is implemented by the MessageHistoryWorflowResolver, which uses the History functionality.

**Note: ** Since this involves a look-up, this is a low-priority Workflow Resolver.

Returning From Controller Methods

In the same way that WorkflowResolvers resolve method parameters, ResponseConverters are able to turn the objects you return from your controllers into Response instances.

Supplied, there are 3 main things you can return from your controller methods. However if you implement a ResponseConverter bean you can extend this:

Return a… What Happens
Response instance This is routed to the ResponseHandlers bean.
List<Response> instance All of those Responses are sent to the ResponseHandlers bean.
@Work-annotated object This is converted into a Response by the WorkResponseConverter, which considers templating from @ChatResponseBody and returns into the chat it came from

Response Handlers

Instead of relying on the return object, you can @Autowire an instance of ResponseHandlers into your controller.

This class aggregates together (in priority order) all of the defined ResponseHandlers defined in your project. With this, you don’t need to return anything from your controller method at all. Instead, you can simply call the accept(Response) method at any point in your controller method.

Content Classes

Before a textual message is handed to your controller method, it is parsed and turned into a hierarchy of Content classes. These are much easier to manipulate and pattern-match with than a piece of plain text, or other markup. Also, the Content class structure is platform-agnostic, which means your method can be too.

We use the Content class structure for parsing messages arriving into your controller method, you can also use it within the method for constructing messages leaving, which can then be formatted in a platform-specific way by an appropriate Response Handler.

Features of Content Objects

Simple Content

Class Purpose
Content A parent interface, indicating something that is read from/can be sent to a chat room
Word A single word. e.g. database
CodeBlock A block of code from the chat
Table A table (like an HTML table)
Tag Platform dependent, but probably a $cashtag, #hashtag or @mention.
User Subclass or Tag (meaning an @mention - note this implements Addressable too, so you can use it to construct Responses.)

OrderedContent

OrderedContent represents things like lists, paragraphs and whole messages:

Class Purpose
OrderedList A numbered list of things.
UnorderedList A bulleted list of things.
Paragraph A list of words, or other bits of content.
Message The whole message from the chat.

Conversations API

You can get details of the chats and 1-1 conversations your bot is engaged in by @Autowire-ing the AllConversations bean into your code. If you are working with multiple chat platforms concurrently, you may need to wire in a platform-specific bean, such as SymphonyConversations or TeamsConversations. This gives you functionality such as:

public interface AllConversations {

  /**
   * Returns all the conversations that the bot is a member of.
   */
  public Set<Addressable> getAllConversations();

  /**
   * Returns the subset of all conversations that are chats.
   */
  public Set<Chat> getAllChats();

  public Chat ensureChat(Chat r, List<User> users, Map<String, Object> meta);
  
  public List<User> getChatMembers(Chat r);
  
  public List<User> getChatAdmins(Chat r);

}

As an example, this API is used by the Reminder Bot, whose TimedAlerter checks each conversation it belongs to on a regular interval to see if there are any reminders it needs to issue in that conversation:

  public void onAllStreams(Consumer<Addressable> action) {
        LOG.info("TimedAlerter waking");
        Set<Addressable> allRooms = conversationsApi.getAllConversations();
        allRooms.forEach(s -> action.accept(s));
        LOG.info("TimedAlerter processed " + allRooms.size() + " streams ");
    }

History

You can pull back historic data from any Addressable by @Autowire-ing the AllHistory bean into your code. If you are working with multiple chat platforms concurrently, you may need to wire in a platform-specific bean, such as SymphonyHistory or TeamsHistory. This gives you functionality such as:

As an example of use, in the Poll Bot there is a button to “End The Poll Now”, which is implemented in this way:

  @ChatButton(value = Poll.class, showWhen = WorkMode.VIEW, buttonText = "End Poll Now")
  public Result end(Poll p, Chat r, History h) {
    List<Answer> responses = h.getFromHistory(Answer.class, p.getId(), null, null);     // get all the answers that have been submitted to the poll
    
    // further code
    

The mechanism for storing history varies on the implementation. For Teams, history is stored in an Azure Blob Storage, wheras with Symphony, the JSON payload of chat messages forms the history.

Addressing Checkers

The AddressingChecker is used to filter requests to your bot. You can

The default InRoomAddressingChecker is used to make sure that when you are chatting to a bot in a room (which may contain lots of other participants), you either mention the bot like so:

 @Bot help

or prefix the command with a slash, like so:

/help

You can override the default AddressingChecker bean with your your own implementation if you want to.

Templating

Templating Content Objects in a MessageResponse

When you send a MessageResponse to a chat, the mesage will get rendered in a chat-platform-specific way. For Symphony this means a rendering to MessageML, whereas Teams has an XML format for this. This is handled by the SymphonyMarkupWriter and TeamsMarkupWriter respectively.

Templating @Work Objects in a WorkResponse

When you send a POJO with the @Work annotation to a chat room, it will get rendered in the room in a platform-specific way. As discussed before, for Symphony this means a Freemarker template. On MS Teams this means an Adaptive Card or in Teams’ XML format. Adaptive Cards are used when the response has input fields, (i.e. edit-mode) whereas XML format is used when the result is view mode. XML Format supports lists and tables which make is more flexible for data displays.

Naming Templates

The SymphonyResponseHandler or TeamsResponseHandler will load the appropriate template file based on the template name from the WorkResponse or @ChatResponseBody annotation. To do this, they use TemplateProviders, which can provide a template in one of the formats we’ve mentioned.

If there is no template, one will be constructed. This is done by reflecting on the fields in the @Work POJO’s class, and building a static template from the class definition. This provides you with a quick way to visualize or edit your data on the chat platform.

Customizing Templates

You can take these generated templates, customize them and save them into your project. This is useful if you want to follow a particular house style, want to provide instructions, or customize the layout of the form.

Platform Location Example
Symphony (View and Edit) classpath:/templates/symphony/*.ftl ‘template-name’ -> classpath:/templates/symphony/template-name.ftl
Teams (Edit) claspspath:/templates/teams/*.json ‘template-name’ -> classpath:/templates/teams/template-name.json
Teams (View) claspspath:/templates/teams/*.html ‘template-name’ -> classpath:/templates/teams/template-name.html

Validation

By default, the templates constructed by the template generator allow for field validation using JSR-380 validation annotations. For example, in the NewClaim class we have:


@Work
public class NewClaim {

  @Min(0)             // stops people entering negative numbers
  Number amount;

When the NewClaim object is created after being passed back to your bot, validation occurs. If the form fails validation, the form is sent back to the user and the fields that didn’t validate are marked with errors.

Supported Property Types

Out-of-the-box support exists for:

Extending Support To New Types

If you want to customize the way in which the Freemarker templates are created (for example, adding support for a new Java Class) you can do this by implementing org.finos.symphony.toolkit.workflow.sources.symphony.handlers.freemarker.TypeConverter. There are plenty of examples of these in the source code. AbstractSimpleTypeConverter is a good thing to extend for this.

Limitations

At the moment, the forms are displayed by walking the class structure and inspecting the declared types (not instance types). For that reason, polymorphism won’t work: only the fields in declared types will be shown.

Help Controller

The HelpController is provided so that any time a user asks /help in a room with your bot, a list of commands is given in a table. This is generated dynamically based on the @ChatRequest-annotated controller methods.

You can exclude a particular command from the help by setting addToHelp=false on the annotation, and you can tailor the description in the help by adding a description attribute to the annotation, like this (which is the controller method used to provide the help):

  @ChatRequest(value = "help", description="Display this help page")
  public Response handleHelp(Addressable a, User u) {