Typesafe Activator

Akka Callcenter

Akka Callcenter

qcentic
Source
February 25, 2014
basics akka scala starter

This is the demonstration of a callcenter based on Akka.

How to get "Akka Callcenter" on your computer

There are several ways to get this template.

Option 1: Choose akka-callcenter in the Typesafe Activator UI.

Already have Typesafe Activator (get it here)? Launch the UI then search for akka-callcenter in the list of templates.

Option 2: Download the akka-callcenter project as a zip archive

If you haven't installed Activator, you can get the code by downloading the template bundle for akka-callcenter.

  1. Download the Template Bundle for "Akka Callcenter"
  2. Extract the downloaded zip file to your system
  3. The bundle includes a small bootstrap script that can start Activator. To start Typesafe Activator's UI:

    In your File Explorer, navigate into the directory that the template was extracted to, right-click on the file named "activator.bat", then select "Open", and if prompted with a warning, click to continue:

    Or from a command line:

     C:\Users\typesafe\akka-callcenter> activator ui 
    This will start Typesafe Activator and open this template in your browser.

Option 3: Create a akka-callcenter project from the command line

If you have Typesafe Activator, use its command line mode to create a new project from this template. Type activator new PROJECTNAME akka-callcenter on the command line.

Option 4: View the template source

The creator of this template maintains it at https://github.com/qcentic/activator-callcenter#master.

Option 5: Preview the tutorial below

We've included the text of this template's tutorial below, but it may work better if you view it inside Activator on your computer. Activator tutorials are often designed to be interactive.

Preview the tutorial

This template demonstrates the creation and operation of a call center.

Overview

Building parts

The scala directory contains the Scala source code. In detail the following files are contained within this directory referring to the main building parts of the application:

In the following sections this tutorial describes these different building parts of the callcenter from a business point of view as well as from a technical point of view.

Business Context

Describes the business context, which means the application background of the callcenter.

Technical Details

Describes the technical background of the call center. This includes the following Akka and Scala specific features:

  • Building Scala composite objects by traits and self type annotations
  • Building Akka supervisor and actor hierarchies
  • Using an Akka Router for broadcasting
  • Using simple Akka state management
  • Using Akka futures
  • Using Akka Asynchronous Call Pattern without futures
  • Using the Typesafe configuration package

Main

The Main-object first of all sets up the call center environment during initialization

    
val system = ActorSystem("CallCenterSimulation")
val company = system.actorOf(Props(Company()), "Company")
val customer = system.actorOf(Props((Customer())), "Customer")
val callCenter = system.actorOf(Props(CallCenter(company)), "CallCenter")
    
In the main-method it triggers an arbitrarly number of customer service requests of the form
    
val (callNumber, skill) = Await.result((customer ? Customer.GetCallNumber).mapTo[(CallNumber, Skill)], 5.seconds)

system.scheduler.scheduleOnce(200.millis) {
  callCenter ! CallCenter.CustomerCall(callNumber,
    skill)
}
    

Typesafe Console

The workload of system can be visualized by the Typesafe Console.

Call Center

Business Context

Once an environment designed to process telephone calls, call centers have evolved to become customer contact centers, processing all types of media transactions. The contact center is now seen as the core of a business due to its critical role in maximizing customer retention by enabling companies to deliver a definitive customer experience. Its integral parts consist of:

  • Representatives who handle customer transactions (also called agents)
  • Management personnel
  • Technology to handle customer transactions, including the routing, treatment, and fulfilment of the transaction. A contact center may be found within a department in a company or it may be the sole business unit that comprises the company.

A call center has a unique reference to a company which manages departments. For each department there exist an agent supervisor which is responsible for a set of agents and the routing of requests to the appropriate agents.

The current template only focuses on the request handling functionality of a call center. This means that the call center services an arbitrary set of telephone numbers which each number belonging to a department inside the company. When a call comes in from a customer the call center analyzes the call number to determine the responsible department. The department specific agent supervisor is then informed to route the request to the appropriate agent depending on the agent skills and the work status of the agent.

Technical Details

General

The CallCenter is the root of an actor hierarchy and describes the main starting point for routing incoming calls from customers. To handle those requests the call center sets up agent supervisors (one for each department) and as a RoutingProvider initializes the Routing, which determines the correct supervisor to route the call to.

Scala Component Structure

The CallCenter structure is a typical example for how Scala-classes are designed throughout this template. The following Scala design principles are used:

  • Self-type annotation: One can use self-type annotations to specify additional types that the self object this can take within the class. In this case there a two traits AgentSupervisorProvider and RoutingProvider used for the annotation. Its almost like saying class CallCenter extends AgentSupervisorProvider with RoutingProvider but saying it implicitly. The compile time dependency is not created at this time. The self-type annotation indicates a promise that the two Provider-classes will be mixed in during any concrete instantiation of the object.
  • Mixin-Composition: In order to make the class flexible enough to for example use it in test as well as in production mode mixin-composition is used.

    Production:

                 
    object CallCenter {
    
          ...
        
          def apply(company: ActorRef) = {
            new CallCenter(company) with ProdAgentSupervisorProvider with ProdRoutingProvider
          }
    }
                  
              
    Test:
                 
    object CallCenter {
    
          ...
        
          def apply(company: ActorRef) = {
            new CallCenter(company) with TestAgentSupervisorProvider with TestRoutingProvider
          }
    }
    
    ...
    val company = system.actorOf(Props(Company()), "Company")
    val callCenter = system.actorOf(Props(CallCenter(company)), "CallCenter")
                  
              

Akka Asynchronous Call Pattern

The DefaultRouting-class uses the Asynchronous Call Pattern, described in Jamie Allen's Book "Effective Akka" to determine the Agent Supervisor for calculating a routing strategy. For each agent supervisor department a separate anonymous actor is used to determine whether the working time of the department is within the range of the current time. If all departments are checked the businessHourCheckComplete-method finishes with the inforation of the still working departments. The RouteAccordingToBusinessHours-message checks whether the computed department according the call number is still open for business and sends the referenced agent supervisor the message to route the call to the appropriate agent.

       
class DefaultRouting(callCenter: ActorRef, agentSuperVisors: Map[String, ActorRef], deptNames: Seq[String]) extends Routing with Actor with ActorLogging {
  import Routing._
  import Company.IsDuringBusinessHours
  import DepartmentBase.generalDept

  def receive: Receive = {
    case Route(callNbr: CallNumber, forSkill: Skill) =>
      val originalSender = self
      // Calculate the responsible department

      val department = callNbr.selectDepartment(deptNames)

      val agentSupervisorInfo = (department, agentSuperVisors(department))

      agentSupervisorInfo match {
        //case (_, EscalationSupervisor) => callCenter ! play music for correct service numbers
        case (_, _) =>

          val callDuringBusinessHoursActor = context.actorOf(Props(new Actor with ActorLogging {

            var duringBusinessHours: Map[String, Boolean] = Map()

            def receive = {
              case DepartmentBase.DuringBusinessHours(deptName, isDuringBusiness) =>
                duringBusinessHours += (deptName -> isDuringBusiness)
                businessHourCheckComplete
              case BusinessHourCheckTimeout =>
                sendResponseAndShutdown(BusinessHourCheckTimeout)
            }

            def businessHourCheckComplete() = {
              val duringBusinessHourKeys = duringBusinessHours.keys.toList
              val complete = (agentSuperVisors.keys.size == duringBusinessHourKeys.size) &&
                (true /: agentSuperVisors.keys) { (res, deptName) =>
                  {
                    if (duringBusinessHourKeys.contains(deptName))
                      res && true
                    else
                      res && false
                  }
                }
              if (complete) {
                timeoutMessager.cancel
                sendResponseAndShutdown(RouteAccordingToBusinessHours(agentSuperVisors, agentSupervisorInfo,
                  duringBusinessHours, forSkill))
              }
            }

            def sendResponseAndShutdown(response: Any) = {
              originalSender ! response
              log.debug("Stopping context capturing actor for businessHourCheck")
              context.stop(self)
            }

            import context.dispatcher
            val timeoutMessager = context.system.scheduler.scheduleOnce(250 milliseconds) {
              self ! BusinessHourCheckTimeout
            }
          }))

          val date = new Date()

          agentSuperVisors.keys foreach {
            deptName => callCenter.tell(Company.IsDuringBusinessHours(deptName, date), callDuringBusinessHoursActor)
          }

      }

    case RouteAccordingToBusinessHours(agentSuperVisors: Map[String, ActorRef],
      agentSupervisor: (String, ActorRef), duringBusinessHours: Map[String, Boolean],
      forSkill: Skill) =>
      duringBusinessHours(agentSupervisor._1) match {

        case true => agentSupervisor._2 ! RoutingStrategyProvider.Route(forSkill)
        case false => agentSuperVisors.filter(supervisor => duringBusinessHours(supervisor._1)).headOption.map(_._2).getOrElse(agentSuperVisors(generalDept)) ! RoutingStrategyProvider.Route(forSkill)
      }
  }

}
       
   

Akka Actor Hierarchy

A CallCenter-object is designed as the root in the actor-hierarchy. For the children of AgentSupervisor's it is using its own restart strategy instead of the normal strategy to restart every single child. it uses the strategy to stop every node in the case of exception. This can easily be changed to the default if the following code-block is removed:

           
override val supervisorStrategy = OneForOneStrategy() {
    case _: ActorInitializationException => Stop
    case _: ActorKilledException => Restart
    case _: Exception => Resume
    case _ => Resume
}              
           
       

Messages

A CallCenter has the following messages:

  • Company.GiveSkillsForDepartment(deptName): An AgentSupervisor sends this message in order to build the agent skill sets. The message is forwarded to the company.
  • Company.IsDuringBusinessHours(deptName, date) The DefaultRouting object sends this message in order to determine whether the call is during the business hours of the department. The message is forwarded to the company.
  • CustomerCall(callNbr: CallNumber, forSkill: Skill): A customer call message is sent by the Main-class and defines the starting point of the process.

Agent

Business Context

An agent offers services (mostly "call services") for private or company customers. One distinguishes between inbound or passive services and outbound or active services. Whereas inbound means that the agent is reacting to a call, for outbound the agent actively calls by himself and offers his services.
The current activator template only handles inbound calls.

According the business she works in every agent has preferred subjects called skills. A skill is combined with a skill priority to make routing of incoming calls to agents possible.

Technical Details

General

A callcenter agent is implemented as an Akka Actor with simple state semantics. The state management is mapped by the Akka become/unbecome feature. That means that by default an agent's receive-method maps to one of the methods handleSpecificTask or handleCall. If routing sends the handleCall-message a one-time scheduled job is started to send takeCall during a scheduled time in the future which simulates the amount of time the agent works on the call. At the same time the receive-method is thread-safely replaced inside the single-threaded actor by the new behaviour which triggers the state change.

Messages

An Agent-object has the following messages it can react to:

  • TakeCall(forSkill: Skill, skillPriorities: List[SkillPriority]): This method is responsible for accepting a call for the questioned skill forSkill and the skill priorities skillPriorities by a RoutingStrategyProvider The skill priorities are used by the asked agent to give the asking skill based routing strategy provider feedback to recalculate a new agent if she is currently busy. In this case the CallNotHandled message is send to the calling routing strategy provider.
  • HandleCall(forSkill: Skill, resultReceiver: ActorRef): This method is responsible for handling a call for the questioned skill forSkill and the receiver resultReceiver (the RoutingStrategyProvider). The result receiver gets informed by sending it the message CallHandled.

Telephone answer agent

In case the routing strategy provider may not find an agent to whom a call might be routed the call is routed to a TelephoneAnswerAgent.

Configuration

Agents are configurable. A typical agent configuration looks like:


com {
    akka {
          agents { 
      		SERVICE =
	      		[[ServiceAgent1, {General = 1, General1 = 3}],
	      		 [ServiceAgent2, {General = 3, General1 = 1}],
	      		 [ServiceAgent3, {General = 2, General1 = 2}],
	      		 [ServiceAgent4, {General = 3, General1 = 3}],
	      		 [ServiceAgent5, {General = 2, General1 = 2}]]
      	}
     }
}
}
An agent is configured per department (in this example "Service") with department specific skills and skill priorities.

Agent Supervisor

Business Context

An Agent Supervisor is responsible for the administration of agents. This includes the following tasks:

  • Provision of agents for handling customer service requests
  • For each agent provision of a skill set, which defines the agent skills and skill priorities. A skill set is used by a routing strategy to choose an agent for service request
  • Definition of a routing strategy, which allows routing of an incoming service request to an agent.

Technical Details

General

An AgentSupervisor is designed as an Akka child node in the CallCenter-Actor hierarchy and as such supervised by the CallCenter-supervisor. The AgentSupervisor uses a Scala component architecture to offer functionalities to provide agents, skill sets and routing strategies. An AgentSupervisor uses simple state semantics to switch between agent-routing and agent-broadcasting modes.

Scala Component Structure
  • Self-type annotation: The traits RoutingStrategyProvider, SkillSetProvider and AgentProvider are used for the annotation.
  • Mixin-Composition: In order to make the class flexible enough to for example use it in test as well as in production mode mixin-composition is used. For concreate instantiation of an object the traits SkillBasedBusyAskRoutingStrategyProvider, ConfigSkillSetProvider and AgentProvider are used. The SkillBasedBusyAskRoutingStrategyProvider uses a Akka-future based algorithm to route calls on the base of agent skills to the agents. The ConfigSkillSetProvider sets up agent skills with the help of the type configuration.

                 
    object AgentSupervisor {
    
      ...
      def apply(callCenter: ActorRef, department: ActorRef) = new AgentSupervisor(callCenter, department) 
                            with SkillBasedBusyAskRoutingStrategyProvider with ConfigSkillSetProvider with AgentProvider
    }
                  
              

Akka Actor Hierarchy

An AgentSupervisor-object sets up a child actor to supervise the children of Agent-objects and another child actor TelephoneAnswerAgent which handles unrouted customer requests. For the children it is using its own OneForOneStrategy

           
override val supervisorStrategy = OneForOneStrategy() {
    case _: ActorKilledException => Restart
    case _: ActorInitializationException => Escalate
    case _ => Resume
}               
           
       
The child actor for the Agent-objects is an anonymous actor which supervises the Agent-nodes with its own supervisor-strategy.
           
override val supervisorStrategy = OneForOneStrategy() {
        case _: ActorKilledException => Restart
        case _: ActorInitializationException => Stop
        case _ => Resume
}               
           
       
With this strategy an agent-node is fault tolerant in the sense that it only stops if something went wrong during initialization.

Messages

An AgentSupervisor-object has the following messages it can react to:

  • SkillsForDepartment(skills): This message is used by the AgentSupervisor itself during the initialization phase to set up the skill set of the department the AgentSupervisor belongs to.
  • GetAgentBroadcaster: Gets send if some object is interested in broadcasting a message to all agents of the AgentSupervisor. Returns a new BroadcastRouter in front of the agents if the supervisor is in state noRouter. If the supervisor is in state withRouter the already existing BroadcastRouter captured in the context is returned.
  • Children(_, destinedFor): If in state withRouter the children are requested the BroadcastRouter is returned.

Agent broadcasting

The anynonymous child actor, which serves as a supervisor for the childs is also used to send back those childs when requested to support the work of a BroadcastRouter.

       
def noRouter: Receive = {
    case GetAgentBroadcaster =>
      import scala.concurrent.ExecutionContext.Implicits.global

      val destinedFor = sender
      val actor = context.actorFor("AgentSupervisor")
      (actor ? GetChildren).mapTo[Seq[ActorRef]].map {
        agents =>
          (Props().withRouter(BroadcastRouter(agents)),
            destinedFor)
      } pipeTo self
    case (props: Props, destinedFor: ActorRef) =>
      log.info(s"AgentSupervisor received (${props.toString()},${destinedFor.toString()}) (transforming to withRouter)")
      val router = context.actorOf(props, "Agents")
      destinedFor ! AgentBroadcaster(router)
      context.become(withRouter(router) orElse route)
       
}
   
The creation of the BroadcastRouter is initiated by transformation of the future result of the GetChildren-messsage via the pipeTo-method. The transformation result is send to self with the request-sender as a parameter.

Company

Business Context

In the context of a call center the company manages departments. A department has working times in terms of business hours and is configured with skills. Skills are requested by customer calls and define the base for skill based routing offered by the company department related agent supervisor.

Technical Details

General

A Company is implemented as an Akka Actor and offers department specific skills. It also gives information about the working times of a department.

Scala Component Structure
  • Self-type annotation: The Company uses the trait DepartmentManager for annotation.
  • Mixin-Composition: For concreate instantiation of an object the trait StandardDepartmentManager is used. The StandardDepartmentManager uses the configuration mechanism from Typesafe to configure departments and department skills.

    Messages

    A Company-object has the following messages it can react to:

    • GiveSkillsForDepartment(deptName: String): Returns the department specific skills to the sender.
    • IsDuringBusinessHours(deptName: String, date: Date): Calculates whether the hours of the given date are within the interval of the business hours of the given department.

    Configuration

    Departments are configurable. A typical department configuration looks like:

    
    com { 
         akka { 
     		company {
          		departments = [[SALES, 7, 21, {Marketing = 3, Marketing1 = 3}], 
          		               [TECHNIC, 8, 21, {Technic = 3, Technic1 = 3}],
          		               [SERVICE, 9, 21, {General = 3, General1 = 3}]
          		              ]
          	}
         }
    }
    }
    A department configuration is described by name, business hours and department skills and priorities.

Customer

Business Context

Customers are requesting company services through telephone numbers, which are specific to the departments of the company.

Technical Details

General

A Customer is implemented as an Akka Actor and offers call numbers as well as skills which reflect the requested services. The call number related department has to offer the skill the customer requests.

Scala Component Structure
  • Self-type annotation: The Customer uses the traits CallNumberProvider and SkillProvider for annotation.
  • Mixin-Composition: For concreate instantiation of an object the traits RandomCallNumberProvider and RandomSkillProvider are used to provide random call numbers and skills.

    Messages

    A Customer-object has the following messages it can react to:

    • GetCallNumber: Returns a call number and a skill to the sender.

comments powered by Disqus