Code Monkey home page Code Monkey logo

electric_slide's Introduction

Electric Slide - Simple Call Distribution for Adhearsion

This library implements a simple call queue for Adhearsion.

To ensure proper operation, a few things are assumed:

  • Agents will only be logged into a single queue at a time If you have two types of agents (say "support" and "sales") then you should have two queues, each with their own pool of agents
  • Agent authentication will happen before entering the queue - it is not the queue's concern
  • The strategy for callers is FIFO: the caller who has been waiting the longest is the next to get an agent
  • Queues will be implemented as a Celluloid Actor, which should protect the call selection strategies against race conditions
  • There are two ways to connect an agent:
    • If the Agent object provides an address attribute, and the queue's connection_type is set to call, then the queue will call the agent when a caller is waiting
    • If the Agent object provides a call attribute, and the queue's connection_type is set to bridge, then the call queue will bridge the agent to the caller. In this mode, the agent hanging up will log him out of the queue

TODO:

  • Example for using Matrioska to offer Agents and Callers interactivity while waiting
  • How to handle MOH

WARNING!

While you can have ElectricSlide keep track of custom queues, it is recommended to use the built-in CallQueue object.

The authors of ElectricSlide recommend NOT to subclass, monkeypatch, or otherwise alter the CallQueue implementation, as the likelihood of creating subtle race conditions is high.

Example Queue

my_queue = ElectricSlide.create :my_queue, ElectricSlide::CallQueue

# Another way to get a handle on a queue
ElectricSlide.create :my_queue
my_queue = ElectricSlide.get_queue :my_queue

Example CallController for Queued Call

class EnterTheQueue < Adhearsion::CallController
  def run
    answer

    # Play music-on-hold to the caller until joined to an agent
    player = play 'http://moh-server.example.com/stream.mp3', repeat_times: 0
    call.on_joined do
      player.stop!
    end

    ElectricSlide.get_queue(:my_queue).enqueue call

    # The controller will exit, but the call will remain up
    # The call will automatically hang up after speaking to an agent
    call.auto_hangup = false
  end
end

Adding an Agent to the Queue

ElectricSlide expects to be instances of the ElectricSlide::Agent class. This is designed to be extended with custom functionality when necessary.

To add an agent who will receive calls whenever a call is enqueued, do something like this:

agent = ElectricSlide::Agent.new id: 1, address: 'sip:[email protected]', presence: :available
ElectricSlide.get_queue(:my_queue).add_agent agent

To inform the queue that the agent is no longer available you must use the ElectricSlide queue interface. Do not attempt to change agent objects directly!

ElectricSlide.update_agent 1, presence: offline

If it is more convenient, you may also pass #update_agent an Agent-like object:

options = {
  id: 1,
  address: 'sip:[email protected]',
  presence: :unavailable
}
agent = ElectricSlide::Agent.new options
ElectricSlide.update_agent 1, agent

The possible presence states for an Agent are:

  • :available - Waiting for a call
  • :on_call - Currently connected to a call
  • :after_call - In a quiet period after completing a call and before being made available again. This is only encountered with a manual agent return strategy.
  • :unavailable - Agent is not available (on break, offline, etc)

Note that an :unavailable agent still counts as an agent in the queue, but will not be sent any calls. Make sure to remove agents, even unavailable ones, when agents sign out by using the #remove_agent method.

Switching connection types

ElectricSlide provides two methods for connecting callers to agents:

  • :call: (default) If the Agent object provides an address attribute, and the queue's connection_type is set to call, then the queue will call the agent when a caller is waiting
  • :bridge: If the Agent object provides a call attribute, and the queue's connection_type is set to bridge, then the call queue will bridge the agent to the caller. In this mode, the agent hanging up will log him out of the queue

To select the connection type, specify it when creating the queue:

ElectricSlide.create_queue :my_queue, ElectricSlide::CallQueue, connection_type: :bridge

Selecting an Agent distribution strategy

Different use-cases have different requirements for selecting the next agent to take a call. ElectricSlide provides two strategies which may be used. You are also welcome to create your own distribution strategy by implementing the same interface as described in ElectricSlide::AgentStrategy::LongestIdle.

To select an agent strategy, specify it when creating the queue:

ElectricSlide.create_queue :my_queue, ElectricSlide::CallQueue, agent_strategy: ElectricSlide::AgentStrategy::LongestIdle

Two strategies are provided out-of-the-box:

  • ElectricSlide::AgentStrategy::LongestIdle selects the agent that has been idle for the longest amount of time.
  • ElectricSlide::AgentStrategy::FixedPriority selects the agent with the lowest numeric priority first. In the event that more than one agent is available at a given priority, then the agent that has been idle the longest at the lowest numeric priority is selected.

Custom Agent Behavior

If you need custom functionality to occur whenever an Agent is selected to take a call, you can use the callbacks on the Agent object:

  • on_connect: Args: [Queue, Agent Call, Client Call] Called as the agent is being connected to the client call
  • on_disconnect: Args: [Queue, Agent Call, Client Call] Called after the agent is disconnected from the client for any reason (eg. hangup)
  • connection_failed: Args: [Queue, Agent Call, Client Call] Called when the agent fails to connect with the client for any reason (eg. no answer)
  • presence_change: Args: [Queue, Agent Call, New Presence] Called after the agent's presence changes

Confirmation Controllers

In case you need to execute a confirmation controller on the call that is placed to the agent, such as "Press 1 to accept the call", you currently need to pass in the confirmation class name and the call object as metadata in the call_options_for callback in your ElectricSlide::Agent subclass.

# an example from the Agent subclass
def dial_options_for(queue, queued_call)
  {
    from: caller_digits(queued_call.from),
    timeout: on_pstn? ? APP_CONFIG.agent_timeout * 3 : APP_CONFIG.agent_timeout,
    confirm: MyConfirmationController,
    confirm_metadata: {caller: queued_call, agent: self},
  }
end

You then need to handle the join in your confirmation controller, using for example:

call.join metadata[:caller] if confirm!

where confirm! is your logic for deciding if you want the call to be connected or not. Hanging up during the confirmation controller or letting it finish without any action will result in the call being sent to the next agent.

Credits

Electric Slide Copyright 2011-2015 Adhearsion Foundation Inc. See the LICENSE file for more information.

Original Author Ben Klang - Mojo Lingo

Contributors:

Also thanks to Power Home Remodeling Group, Teleforge, and Atlanta Game Adventures for sponsoring development.

electric_slide's People

Contributors

benlangfeld avatar bklang avatar lpradovera avatar nehaabraham avatar neildecapia avatar ns-ian avatar rafbgarcia avatar system123 avatar taylor avatar terryfinn avatar victorluft avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

electric_slide's Issues

Log the correct caller ID for OutboundCalls

In CallQueue when logging that an agent is talking to a caller, we use call#from. This is correct when the call is inbound. But for an OutboundCall, this is actually showing the caller ID we used when placing the call, instead of the intended called party.

misnamed available agent method in LongestIdle

When calling queue.available_agent_summary the following exception occurs.

NoMethodError: undefined method `available_agent_summary' for #ElectricSlide::AgentStrategy::LongestIdle:0x228690e3

This is as the method is called available_agent_count not available_agent_summary.

Presence callback cannot access agent object, after hangup.

I'm using Electric Slide with bridging and manual agent return. I require the presence callback for the agent going into "unavailable" state, in order to initiate a cleanup sequence in my code. The issue lies in the "bridged_agent_health_check" method.

agent.call.on_end do
  agent.call = nil
  queue.return_agent agent, :unavailable
end

The return_agent method fires the "on_presence_changed" callback with parameters: queue, agent_call, new_presence, old_presence

As can be seen the agent_call will be nil in this case, and thus I cannot access the agent object in the callback handler as I normally do "agent_call[:agent]".

I am happy to make the fix, just want to discuss the best solution. I would recommend we return the agent in all callbacks instead of agent_call. The call object can always be accessed via the agent object. Would this be an acceptable fix?

Queue actor should not crash when handling dead calls

[2015-02-17 22:19:54.985] ERROR Celluloid: ElectricSlide::CallQueue crashed!
Adhearsion::Call::ExpiredError: This call is expired and is no longer accessible. See http://adhearsion.com/docs/calls for further details.
    /var/void/shared/bundle/jruby/1.9/gems/adhearsion-2.5.4/lib/adhearsion/call.rb:28:in `method_missing'
    /var/void/shared/bundle/jruby/1.9/gems/adhearsion-2.5.4/lib/adhearsion/call.rb:26:in `method_missing'
    /var/void/releases/20150217191654/app/models/agent.rb:207:in `to_s'
    /var/void/shared/bundle/jruby/1.9/bundler/gems/electric_slide-466dcce170db/lib/electric_slide/call_queue.rb:118:in `remove_agent'
    org/jruby/RubyKernel.java:1945:in `public_send'
    /var/void/shared/bundle/jruby/1.9/gems/activesupport-4.2.0/lib/active_support/core_ext/object/try.rb:77:in `try!'
    /var/void/shared/bundle/jruby/1.9/gems/activesupport-4.2.0/lib/active_support/core_ext/object/try.rb:63:in `try'
    org/jruby/RubyKernel.java:1945:in `public_send'
    /var/void/shared/bundle/jruby/1.9/gems/celluloid-0.15.0/lib/celluloid/calls.rb:25:in `dispatch'
    /var/void/shared/bundle/jruby/1.9/gems/celluloid-0.15.0/lib/celluloid/calls.rb:67:in `dispatch'
    /var/void/shared/bundle/jruby/1.9/gems/celluloid-0.15.0/lib/celluloid/actor.rb:322:in `handle_message'
    /var/void/shared/bundle/jruby/1.9/gems/celluloid-0.15.0/lib/celluloid/actor.rb:416:in `task'
    /var/void/shared/bundle/jruby/1.9/gems/celluloid-0.15.0/lib/celluloid/tasks.rb:54:in `initialize'
    /var/void/shared/bundle/jruby/1.9/gems/celluloid-0.15.0/lib/celluloid/tasks.rb:46:in `initialize'
    /var/void/shared/bundle/jruby/1.9/gems/celluloid-0.15.0/lib/celluloid/tasks/task_fiber.rb:13:in `create'

#enqueue should be blocking on the caller

Most Adhearsion operations are blocking: if you start #play, or #ask (for example) the caller is blocked until the method returns some value. This makes developing easier for a lot of use-cases because you don't have to think about dealing with asynchronous behavior.

Right now, ES's #enqueue method is non-blocking. It should be blocking by default, and the documentation should show how to call the method both sync and async.

Call Queue crash on agent_checkout

ES crashes when trying to checkout an agent that may already have ended their call, or if the current free agent list is empty. Checks need to be put in place to prevent the queue crashing when this occurs, rather just fail safely and silently.

[2015-02-26 14:42:28.961] ERROR Adhearsion::OutboundCall: a645f118-8605-41c3-87dc-747c6574eb5e@: undefined method presence=' for nil:NilClass /var/void/shared/bundle/jruby/1.9/bundler/gems/electric_slide-4ab90efbd422/lib/electric_slide/call_queue.rb:58:incheckout_agent'
/var/void/shared/bundle/jruby/1.9/bundler/gems/electric_slide-4ab90efbd422/lib/electric_slide/call_queue.rb:138:in check_for_connections' /var/void/shared/bundle/jruby/1.9/bundler/gems/electric_slide-4ab90efbd422/lib/electric_slide/call_queue.rb:162:inenqueue'
/var/void/shared/bundle/jruby/1.9/bundler/gems/electric_slide-4ab90efbd422/lib/electric_slide/call_queue.rb:226:in ignoring_ended_calls' /var/void/shared/bundle/jruby/1.9/bundler/gems/electric_slide-4ab90efbd422/lib/electric_slide/call_queue.rb:157:inenqueue'
org/jruby/RubyKernel.java:1958:in public_send' /var/void/shared/bundle/jruby/1.9/gems/celluloid-0.15.2/lib/celluloid/calls.rb:25:indispatch'
/var/void/shared/bundle/jruby/1.9/gems/celluloid-0.15.2/lib/celluloid/calls.rb:67:in dispatch' /var/void/shared/bundle/jruby/1.9/gems/celluloid-0.15.2/lib/celluloid/actor.rb:322:inhandle_message'
/var/void/shared/bundle/jruby/1.9/gems/celluloid-0.15.2/lib/celluloid/actor.rb:416:in `task'

Caller failing to connect gets inserted into the queue multiple times

Example from logs:

[2016-12-01 15:48:15.904] INFO  Adhearsion::OutboundCall: d7171279-cb7a-4738-935f-0b46633a2a5d@: Call 4804821680 -> SIP/000 ended due to error
[2016-12-01 15:48:15.906] INFO  ElectricSlide::CallQueue: Returning agent 15685 to queue
[2016-12-01 15:48:15.906] DEBUG ElectricSlide::CallQueue: Returning #<PowerSlide::SupportNinja id: "15685", name: "Connor G.", presence: :on_call, number: "000", priority: 0> to the queue
[2016-12-01 15:48:15.912] INFO  ElectricSlide::CallQueue: Connecting #<PowerSlide::SupportNinja id: "15685", name: "Connor G.", presence: :on_call, number: "000", priority: 0> with unknown <SIP/fake-pbx>
[2016-12-01 15:48:15.944] DEBUG Adhearsion::OutboundCall: fc0cb5f9-d5eb-4c50-8323-9ac53fbef3da@: Executing command #<Punchblock::Command::Dial target_call_id=nil, target_mixer_name=nil, component_id=nil, source_uri=nil, domain=nil, transport=nil, timestamp=Thu, 01 Dec 2016 15:48:15 -0500, request_id="80417525-6a02-44e3-baaf-dfcfb61795c0", headers={}, to="SIP/000", from="4804821680", uri="fc0cb5f9-d5eb-4c50-8323-9ac53fbef3da", timeout=30000, join=nil>
[2016-12-01 15:48:15.961] WARN  ElectricSlide::CallQueue: Call did not connect to agent! Agent 15685 call ended with error; reinserting caller unknown <SIP/fake-pbx> into queue
[2016-12-01 15:48:16.170] DEBUG Adhearsion::OutboundCall: fc0cb5f9-d5eb-4c50-8323-9ac53fbef3da@: Receiving message: #<Punchblock::Event::End target_call_id="fc0cb5f9-d5eb-4c50-8323-9ac53fbef3da", target_mixer_name=nil, component_id=nil, source_uri=nil, domain=nil, transport=nil, timestamp=Thu, 01 Dec 2016 20:48:16 +0000, headers={}, reason=:error, platform_code=nil>
[2016-12-01 15:48:16.170] INFO  Adhearsion::OutboundCall: fc0cb5f9-d5eb-4c50-8323-9ac53fbef3da@: Call 4804821680 -> SIP/000 ended due to error
[2016-12-01 15:48:16.172] INFO  ElectricSlide::CallQueue: Returning agent 15685 to queue
[2016-12-01 15:48:16.173] DEBUG ElectricSlide::CallQueue: Returning #<PowerSlide::SupportNinja id: "15685", name: "Connor G.", presence: :on_call, number: "000", priority: 0> to the queue
[2016-12-01 15:48:16.179] INFO  ElectricSlide::CallQueue: Connecting #<PowerSlide::SupportNinja id: "15685", name: "Connor G.", presence: :on_call, number: "000", priority: 0> with unknown <SIP/fake-pbx>
[2016-12-01 15:48:16.200] DEBUG Adhearsion::OutboundCall: 4d39c1d8-baae-4f72-ad0e-bb149c7ee5ae@: Executing command #<Punchblock::Command::Dial target_call_id=nil, target_mixer_name=nil, component_id=nil, source_uri=nil, domain=nil, transport=nil, timestamp=Thu, 01 Dec 2016 15:48:16 -0500, request_id="9d17ff81-9ae6-42ce-84d2-3ef19f7ff3b3", headers={}, to="SIP/000", from="4804821680", uri="4d39c1d8-baae-4f72-ad0e-bb149c7ee5ae", timeout=30000, join=nil>
[2016-12-01 15:48:16.223] WARN  ElectricSlide::CallQueue: Call did not connect to agent! Agent 15685 call ended with error; reinserting caller unknown <SIP/fake-pbx> into queue
[2016-12-01 15:48:16.377] INFO  Virginia: 127.0.0.1 - - [01/Dec/2016:15:48:16 -0500] "GET /call_queues/nitro_ninja " 200 415 0.0390
[2016-12-01 15:48:16.465] DEBUG Adhearsion::Call: 8f1e73b6-f06f-4dee-bc5d-e6210b71f34d@: Receiving message: #<Punchblock::Event::End target_call_id="8f1e73b6-f06f-4dee-bc5d-e6210b71f34d", target_mixer_name=nil, component_id=nil, source_uri=nil, domain=nil, transport=nil, timestamp=Thu, 01 Dec 2016 20:48:16 +0000, headers={}, reason=:hungup, platform_code="0">
[2016-12-01 15:48:16.466] INFO  Adhearsion::Call: 8f1e73b6-f06f-4dee-bc5d-e6210b71f34d@: Call unknown <SIP/fake-pbx> -> inbound_nitro_support ended due to hungup (code 0)
[2016-12-01 15:48:16.493] DEBUG Adhearsion::OutboundCall: 4d39c1d8-baae-4f72-ad0e-bb149c7ee5ae@: Receiving message: #<Punchblock::Event::End target_call_id="4d39c1d8-baae-4f72-ad0e-bb149c7ee5ae", target_mixer_name=nil, component_id=nil, source_uri=nil, domain=nil, transport=nil, timestamp=Thu, 01 Dec 2016 20:48:16 +0000, headers={}, reason=:error, platform_code=nil>
[2016-12-01 15:48:16.495] INFO  Adhearsion::OutboundCall: 4d39c1d8-baae-4f72-ad0e-bb149c7ee5ae@: Call 4804821680 -> SIP/000 ended due to error
[2016-12-01 15:48:16.513] INFO  ElectricSlide::CallQueue: Returning agent 15685 to queue
[2016-12-01 15:48:16.514] DEBUG ElectricSlide::CallQueue: Returning #<PowerSlide::SupportNinja id: "15685", name: "Connor G.", presence: :on_call, number: "000", priority: 0> to the queue
[2016-12-01 15:48:16.523] WARN  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> hung up before being connected to an agent.
[2016-12-01 15:48:16.999] DEBUG ClientHelper: HTTP POST to http://127.0.0.1:3000/api/v1/support_calls/ WITH PARAMS {:original_recording_location=>"/var/spool/asterisk/monitor/support_1480625263_725667.wav", :caller_id=>"unknown <SIP/fake-pbx>", :duration=>34, :was_answered=>true, :user_id=>nil, :support_agent_id=>nil, :lead_id=>nil, :lead_type=>nil, :call_result=>:entering_queue} RESULT 200:  " "
[2016-12-01 15:48:17.001] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.004] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.005] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.007] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.010] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.015] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.017] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.033] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.036] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.041] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.043] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.046] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.048] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.051] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.055] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue
[2016-12-01 15:48:17.062] INFO  ElectricSlide::CallQueue: Caller unknown <SIP/fake-pbx> has abandoned the queue

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.