<!doctype html>
<html>
{{ template "partials/header_partial" . }}
<div id="primaryContent" class="pageContent">
{{ template "pagecontent" . }}
</div>
<div id="chatboxContainer" class="containerPulse">
</div>
{{ template "partials/footer_partial" . }}
</html>
The chat box will be implemented as a partial template in the chatbox_partial.tmpl source file in the shared/templates/partials folder:
<div id="chatbox">
<div id="chatboxHeaderBar" class="chatboxHeader">
<div id="chatboxTitle" class="chatboxHeaderTitle"><span>Chat
with
{{.AgentName}}</span></div>
<div id="chatboxCloseControl">X</div>
</div>
<div class="chatboxAgentInfo">
<div class="chatboxAgentThumbnail"><img
src="{{.AgentThumbImagePath}}"
height="81px"></div>
<div class="chatboxAgentName">{{.AgentName}}</div>
<div class="chatboxAgentTitle">{{.AgentTitle}}</div>
</div>
<div id="chatboxConversationContainer">
</div>
<div id="chatboxMsgInputContainer">
<input type="text" id="chatboxInputField" placeholder="Type your message here...">
</input>
</div>
<div class="chatboxFooter">
<a href="http://www.isomorphicgo.org" target="_blank">Powered by
Isomorphic Go</a>
</div>
</div>
This is the HTML markup required to implement the wireframe design of the live chat box. Note that the input textfield having the id "chatboxInputField".
This is the input field where the user will be able to type their message. Each message
created, both the one that the user writes, as well as the one that the bot writes, will use the livechatmsg_partial.tmpl template:
<div class="chatboxMessage">
<div class="chatSenderName">{{.Name}}</div>
<div class="chatSenderMsg">{{.Message}}</div>
</div>
Each message is inside its own div container that has two div containers (shown in bold) housing the name of the sender of the message and the message itself.
There are no buttons necessary in the live chat feature, since we will be adding an event listener to listen for the press of the Enter key to submit the user's message to the server over the WebSocket connection.
Now that we've implemented the HTML markup that will be used to render the chat box, let's examine the functionality required to implement the live chat feature on the server side.
When the live chat feature is activated, we will create a persistent, WebSocket connection, between the web client and the web server. The Gorilla Web Toolkit provides an excellent implementation of the WebSocket protocol in their websocket package. To fetch the websocket package, you may issue the following command:
$ go get github.com/gorilla/websocket
The Gorilla web toolkit also provides a useful example web chat application.
Rather than reinventing the wheel, we will repurpose Gorilla's example web chat application to fulfill the live chat feature. The source files needed from the web chat example have been copied over to the chat folder.
There are three major changes we need to make to realize the live chat feature using the example chat application provided by Gorilla:
Let's consider each of these three points in more detail.
First, Gorilla's web chat example is a free-for-all chat room. Any user can come in, type a message, and all other users connected to the chat server will be able to see the message. A major requirement for the live chat feature is that each conversation between the chatbot and the human should be exclusive. Replies from the agent must be targeted to a specific user, and not to all connected users.
Second, the example web chat application from the Gorilla web toolkit doesn't send any messages back to the user. This is where the custom chatbot comes into the picture. The agent will communicate directly with the user over the established WebSocket connection.
Third, the front-end portion of the example web chat application is implemented as a HTML document containing inline CSS and JavaScript. As you may have guessed already, we will implement the front-end portion for the live chat feature in Go, and the code will reside in the client/chat folder.
Now that we have established our plan of action to implement the live chat feature using the Gorilla web chat example as a foundation to start from, let's begin the implementation.
The modified web chat application that we will create contains two main types: Hub and
Client.
The chat hub is responsible for maintaining a list of client connections and directing the chatbot to broadcast a message to the relevant client. For example, if Alice asked the question "What is Isomorphic Go?", the answer from the chatbot should go to Alice and not to Bob (who may not have even asked a question yet).
Here's what the Hub struct looks like:
type Hub struct {
chatbot bot.Bot
clients map[*Client]bool
broadcastmsg chan *ClientMessage register chan *Client
unregister chan *Client
}
The chatbot is a chat bot (agent) that implements the Bot interface. This is the brain that will answer the questions received from clients.
The clients map is used to register clients. The key-value pair stored in the map consists of the key, a pointer to a Client instance, and the value consists of a Boolean value set to
true to indicate that the client is connected. Clients communicate with the hub over the broadcastmsg, register, and unregister channels. The register channel registers a client with the hub. The unregister channel, unregisters a client with the hub. The client sends the message entered by the user over the broadcastmsg channel, a channel of type ClientMessage. Here's the ClientMessage struct that we have introduced:
type ClientMessage struct {
client *Client
message []byte
}
To fulfill the first major change we laid out previously, that is, the exclusivity of the conversation between the agent and the user, we use the ClientMessage struct to store, both the pointer to the Client instance that sent the user's message along with the user's message itself (a byte slice).
The constructor function, NewHub, takes in chatbot that implements the Bot interface and returns a new Hub instance:
func NewHub(chatbot bot.Bot) *Hub {
return &Hub{
chatbot: chatbot,
broadcastmsg: make(chan *ClientMessage), register: make(chan
*Client), unregister:
make(chan *Client),
clients: make(map[*Client]bool),
}
}
We implement an exported getter method, ChatBot, so that the chatbot can be accessed from the Hub object:
func (h *Hub) ChatBot() bot.Bot {
return h.chatbot
}
This action will be significant when we implement a Rest API endpoint to send the bot's details (its name, title, and avatar image) to the client.
The SendMessage method is responsible for broadcasting a message to a particular client:
func (h *Hub) SendMessage(client *Client, message []byte) {
client.send <- message
}
The method takes in a pointer to Client, and the message, which is a byte slice, that should be sent to that particular client. The message will be sent over the client's send channel.
The Run method is called to start the chat hub:
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
greeting := h.chatbot.Greeting()
h.SendMessage(client, []byte(greeting))
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case clientmsg := <-h.broadcastmsg:
client := clientmsg.client
reply := h.chatbot.Reply(string(clientmsg.message))
h.SendMessage(client, []byte(reply))
}
}
}
We use the select statement inside the for loop to wait on multiple client operations.
In the case that a pointer to a Client comes in over the hub's register channel, the hub will register the new client by adding the client pointer (as the key) to the clients map and set a value of true for it. We will fetch a greeting message to return to the client by
calling the Greeting method on chatbot. Once we get the greeting (a string value), we call the SendMessage method passing in the client and the greeting converted to a byte slice.
In the case that a pointer to a Client comes in over the hub's unregister channel, the hub will remove the entry in map for the given client and close the client's send channel, which signifies that the client won't be sending any more messages to the server.
In the case that a pointer to a ClientMessage comes in over the hub's broadcastmsg channel, the hub will pass the client's message (as a string value) to the Reply method of the chatbot object. Once we get reply (a string value) from the agent, we call the SendMessage method passing in the client and the reply converted to a byte slice.
The Client type acts as a broker between Hub and the websocket connection. Here's what the Client struct looks like:
type Client struct {
hub *Hub
conn *websocket.Conn send chan []byte
}
Each Client value contains a pointer to Hub, a pointer to a websocket connection, and a buffered channel, send, meant for outbound messages.
The readPump method is responsible for relaying inbound messages coming in over the
websocket connection to the hub:
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c c.conn.Close()
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := c.conn.ReadMessage() if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) { log.Printf("error:
%v", err)
}
break
}
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
// c.hub.broadcast <- message
clientmsg := &ClientMessage{client: c, message: message}
c.hub.broadcastmsg <- clientmsg
}
}
We had to make a slight change to this function to fulfill the requirements of the live chat feature. In the Gorilla web chat example, the message alone was relayed over to Hub. Since we are directing chat bot responses, back to the client that sent them, not only do we need to send the message to the hub, but also the client that happened to send the message (shown in bold). We do so by creating a ClientMessage struct:
type ClientMessage struct {
client *Client
message []byte
}
The ClientMessage struct contains fields to hold both the pointer to the client as well as the message, a byte slice.
Going back to the readPump function in the client.go source file, the following two lines are instrumental in allowing the Hub to know which client sent the message:
clientmsg := &ClientMessage{client: c, message: message}
c.hub.broadcastmsg <- clientmsg
The writePump method is responsible for relaying outbound messages from the client's
send channel over the websocket connection:
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// The hub closed the channel. c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
// Add queued chat messages to the current websocket message.
n := len(c.send)
for i := 0; i < n; i++ {
w.Write(newline)
w.Write(<-c.send)
}
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, []byte{}); err
!= nil {
return
}
}
}
}
The ServeWS method is meant to be registered as an HTTP handler by the web application:
func ServeWs(hub *Hub) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { log.Println(err) return
}
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
client.hub.register <- client
go client.writePump() client.readPump()
})
}
This method performs two important tasks. The method upgrades the normal HTTP connection to a websocket connection and registers the client to the hub.
Now that we've set up the code for our web chat server, it's time to activate it in our web application.
In the igweb.go source file, we have included a function called startChatHub, which is responsible for starting the Hub:
func startChatHub(hub *chat.Hub) {
go hub.Run()
}
We add the following code in the main function to create a new chatbot, associate it with the Hub, and start the Hub:
chatbot := bot.NewAgentCase()
hub := chat.NewHub(chatbot)
startChatHub(hub)
When we call the registerRoutes function to register all the routes for the server-side web application, note that we also pass in the hub value to the function:
r := mux.NewRouter()
registerRoutes(&env, r, hub)
In the registerRoutes function, we need the hub to register the route handler for the Rest API endpoint that returns the agent's information:
r.Handle("/restapi/get-agent-info", endpoints.GetAgentInfoEndpoint(env,
hub.ChatBot()))
The hub is also used to register the route handler for the WebSocket route, /ws. We register the ServeWS handler function, passing in the hub:
r.Handle("/ws", chat.ServeWs(hub))
Now that we have everything in place to activate the chat server, it's time to focus on the star of the live chat feature—the chat agent.
The chat bot type that we will use for the live chat feature, AgentCase, will implement the following Bot interface:
type Bot interface {
Greeting() string
Reply(string) string
Name() string
Title() string
ThumbnailPath() string
SetName(string)
SetTitle(string)
SetThumbnailPath(string)
}
The Greeting method will be used to send an initial greeting to the user, enticing them to interact with the chat bot.
The Reply method accepts a question (a string) and returns a reply (also a string) for the given question.
The rest of the methods implemented are for purely psychological reasons to give humans the illusion that they are communicating with someone, rather than something.
The Name method is a getter method that returns the chat bot's name. The Title method is a getter method that returns the chat bot's title. The ThumbnailPath method is a getter method that returns the path to the chat bot's avatar image.
Each of the getter methods has a corresponding setter method: SetName, SetTitle, and
SetThumbnailPath.
By defining the Bot interface, we are clearly stating the expectations of a chat bot. This allows us to make the chat bot solution extensible in the future. For example, the intelligence that Case exhibits may be too rudimentary and limiting. In the near future, we may want to implement a bot named Molly, whose intelligence may be implemented using a more powerful algorithm. As long as the Molly chat bot implements the Bot interface, the new chat bot can be easily plugged into our web application.
In fact, from the perspective of the server-side web application, it would just be a one-line code change. Instead of instantiating an AgentCase instance, we would instantiate an AgentMolly instance instead. Besides the difference in intelligence, the new chat bot, Molly, would come with its own name, title, and avatar image, so humans would be able to differentiate it from Case.
Here's the AgentCase struct:
type AgentCase struct {
Bot
name string
title string
thumbnailPath string
knowledgeBase map[string]string
knowledgeCorpus []string
sampleQuestions []string
}
We have embedded the Bot interface to the struct definition, indicating that the AgentCase type will implement the Bot interface. The field name is for the name of the agent. The field title is for the title of the agent. The field thumbnailPath is used to specify the path to the chat bot's avatar image.
The knowledgeBase field is map of type map[string]string. This is essentially the agent's brain. Keys in the map are the common terms found in a particular question. Values in the map are the answers to the question.
The knowledgeCorpus field, a string byte slice, is a knowledge corpus of the terms that may exist in questions that the bot will be asked. We use the keys of the knowledgeBase map to construct the knowledgeCorpus. A corpus is a collection of text that is used to conduct linguistic analysis. In our case, we will conduct the linguistic analysis based on the question (the query) that the human user provided to the bot.
The sampleQuestions field, a string byte slice, will contain a list of sample questions that the user may ask the chat bot. The chat bot will provide the user with a sample question when it greets them to entice the human user into a conversation. It is understood that the human user is free to paraphrase the sample question or ask an entirely different question depending on their preference.
The initializeIntelligence method is used to initialize Case's brain:
func (a *AgentCase) initializeIntelligence() {
a.knowledgeBase = map[string]string{
"isomorphic go isomorphic go web applications": "Isomorphic Go is the methodology to create isomorphic web applications using the Go (Golang) programming language. An isomorphic web application, is a web application, that contains code which can run, on both the web client and the web server.",
"kick recompile code restart web server instance instant kickstart lightweight mechanism": "Kick is a lightweight mechanism to provide an instant kickstart to a Go web server instance, upon the modification of a Go source file within a particular project directory (including any subdirectories). An instant kickstart consists of a recompilation of the Go code and a restart of the web server instance. Kick comes with the ability to take both the go and gopherjs commands into consideration when performing the instant kickstart. This makes it a really handy tool for isomorphic golang projects.",
"starter code starter kit": "The isogoapp, is a basic, barebones web app, intended to be used as a starting point for developing an Isomorphic Go application. Here's the link to the github page: https://github.com/isomorphicgo/isogoapp",
"lack intelligence idiot stupid dumb dummy don't know anything": "Please don't question my intelligence, it's artificial after all!",
"find talk topic presentation lecture subject": "Watch the Isomorphic Go talk by Kamesh Balasubramanian at GopherCon India: https://youtu.be/zrsuxZEoTcs",
"benefits of the technology significance of the technology importance
of the technology": "Here are some benefits of Isomorphic Go: Unlike JavaScript, Go provides type safety, allowing us to find and eliminate many bugs at compile time itself. Eliminates mental context-shifts between back- end and front-end coding. Page loading prompts are not necessary.",
"perform routing web app register routes define routes": "You can implement client-side routing in your web application using the isokit Router preventing the dreaded full page reload.",
"render templates perform template rendering": "Use template sets, a set of project templates that are persisted in memory and are available on both the server-side and the client-side",
"cogs reusable components react-like react": "Cogs are reuseable components in an Isomorphic Go web application.",
}
a.knowledgeCorpus = make([]string, 1) for k, _ := range a.knowledgeBase {
a.knowledgeCorpus = append(a.knowledgeCorpus, k)
}
a.sampleQuestions = []string{"What is isomorphic go?", "What are the benefits of this
technology?", "Does isomorphic go offer anything react- like?", "How can I recompile code
instantly?", "How can I perform routing in my web app?", "Where can I get starter code?",
"Where can I find a talk on this topic?"}
}
There are three important tasks that occur within this method:
The first task we must take care of is to set Case's knowledge base. This consists of setting the knowledgeBase property of the AgentCase instance. As mentioned earlier, the keys in the map refer to terms found in the question, and the values in the map are the answers to the question. For example, the "isomorphic go isomorphic go web applications" key could service the following questions:
Due to the the large amount of text contained within the map literal declaration for the knowledgeBase map, I encourage you to view the source file, agentcase.go, on a computer. |
The second task we must take care of is to set Case's corpus, the collection of text used for linguistic analysis used against the user's question. The corpus is constructed from the keys of the knowledgeBase map. We set the knowledgeCorpus field property of the AgentCase instance to a newly created string byte slice using the built-in make function.
Using a for loop, we iterate through all the entries in the knowledgeBase map and append each key to the knowledgeCorpus field slice.
The third and last task we must take care of is to set the sample questions that Case will present to the human user. We simply populate the sampleQuestions property of the AgentCase instance. We use the string literal declaration to populate all the sample questions that are contained in the string byte slice.
Here are the getter and setter methods of the AgentCase type:
func (a *AgentCase) Name() string {
return a.name
}
func (a *AgentCase) Title() string {
return a.title
}
func (a *AgentCase) ThumbnailPath() string {
return a.thumbnailPath
}
func (a *AgentCase) SetName(name string) {
a.name = name
}
func (a *AgentCase) SetTitle(title string) {
a.title = title
}
func (a *AgentCase) SetThumbnailPath(thumbnailPath string) {
a.thumbnailPath = thumbnailPath
}
These methods are used to get and set the name, title, and thumbnailPath fields of the AgentCase object.
Here's the constructor function used to create a new AgentCase instance:
func NewAgentCase() *AgentCase {
agentCase := &AgentCase{name: "Case", title: "Resident Isomorphic
Gopher Agent",
thumbnailPath: "/static/images/chat/Case.png"}
agentCase.initializeIntelligence() return agentCase
}
We declare and initialize the agentCase variable with a new AgentCase instance, setting the fields for name, title, and thumbnailPath. We then call the initializeIntelligence method to initialize Case's brain. Finally, we return the newly created and initialized AgentCase instance.
To summarize, we introduced you to the websocket package from the Gorilla toolkit project. We learned how to establish a persistent connection between the web server and the web client to create a server-side chatbot using WebSocket functionality.
You read an excerpt from a book written by Kamesh Balasubramanian titled Isomorphic Go. In this book, you will learn how to build and deploy Isomorphic Go web applications.
Top 4 chatbot development frameworks for developers
How to create a conversational assistant or chatbot using Python
Build a generative chatbot using recurrent neural networks (LSTM RNNs)