call_center 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.travis.yml +3 -0
- data/README.md +94 -78
- data/VERSION +1 -1
- data/call_center.gemspec +5 -2
- data/lib/call_center.rb +1 -0
- data/lib/call_center/conditional_stack.rb +51 -0
- data/lib/call_center/state_machine_ext.rb +35 -0
- data/test/call_center_test.rb +82 -0
- data/test/examples/dynamic_transition_call.rb +41 -0
- metadata +7 -4
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -12,62 +12,66 @@ Call Center streamlines the process of defining multi-party call workflows in yo
|
|
12
12
|
Usage
|
13
13
|
-----
|
14
14
|
|
15
|
-
|
16
|
-
|
15
|
+
```ruby
|
16
|
+
class Call
|
17
|
+
include CallCenter
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
state :incoming do
|
24
|
-
response do |x|
|
25
|
-
x.Gather :numDigits => '1', :action => customer(:wants_voicemail) do
|
26
|
-
x.Say "Hello World"
|
27
|
-
x.Play some_nice_music, :loop => 100
|
28
|
-
end
|
29
|
-
# <?xml version="1.0" encoding="UTF-8" ?>
|
30
|
-
# <Response>
|
31
|
-
# <Gather numDigits="1" action="/voice/calls/flow?event=wants_voicemail&actor=customer&call_id=5000">
|
32
|
-
# <Say>Hello World</Say>
|
33
|
-
# <Play loop="100">http://some.nice.music.com/1.mp3</Play>
|
34
|
-
# </Gather>
|
35
|
-
# </Response>
|
36
|
-
end
|
37
|
-
|
38
|
-
event :called, :to => :routing, :if => :agents_available?
|
39
|
-
event :called, :to => :voicemail
|
40
|
-
event :wants_voicemail, :to => :voicemail
|
41
|
-
event :customer_hangs_up, :to => :cancelled
|
42
|
-
end
|
19
|
+
call_flow :state, :intial => :incoming do
|
20
|
+
actor :customer do |call, event|
|
21
|
+
"/voice/calls/flow?event=#{event}&actor=customer&call_id=#{call.id}"
|
22
|
+
end
|
43
23
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
# <Response>
|
50
|
-
# <Say>Please leave a message</Say>
|
51
|
-
# <Record action="/voice/calls/flow?event=voicemail_complete&actor=customer&call_id=5000"/>
|
52
|
-
# </Response>
|
53
|
-
end
|
54
|
-
|
55
|
-
event :voicemail_complete, :to => :voicemail_completed
|
56
|
-
event :customer_hangs_up, :to => :cancelled
|
24
|
+
state :incoming do
|
25
|
+
response do |x|
|
26
|
+
x.Gather :numDigits => '1', :action => customer(:wants_voicemail) do
|
27
|
+
x.Say "Hello World"
|
28
|
+
x.Play some_nice_music, :loop => 100
|
57
29
|
end
|
30
|
+
# <?xml version="1.0" encoding="UTF-8" ?>
|
31
|
+
# <Response>
|
32
|
+
# <Gather numDigits="1" action="/voice/calls/flow?event=wants_voicemail&actor=customer&call_id=5000">
|
33
|
+
# <Say>Hello World</Say>
|
34
|
+
# <Play loop="100">http://some.nice.music.com/1.mp3</Play>
|
35
|
+
# </Gather>
|
36
|
+
# </Response>
|
37
|
+
end
|
58
38
|
|
59
|
-
|
39
|
+
event :called, :to => :routing, :if => :agents_available?
|
40
|
+
event :called, :to => :voicemail
|
41
|
+
event :wants_voicemail, :to => :voicemail
|
42
|
+
event :customer_hangs_up, :to => :cancelled
|
43
|
+
end
|
60
44
|
|
61
|
-
|
45
|
+
state :voicemail do
|
46
|
+
response do |x|
|
47
|
+
x.Say "Please leave a message"
|
48
|
+
x.Record(:action => customer(:voicemail_complete))
|
49
|
+
# <?xml version="1.0" encoding="UTF-8" ?>
|
50
|
+
# <Response>
|
51
|
+
# <Say>Please leave a message</Say>
|
52
|
+
# <Record action="/voice/calls/flow?event=voicemail_complete&actor=customer&call_id=5000"/>
|
53
|
+
# </Response>
|
62
54
|
end
|
55
|
+
|
56
|
+
event :voicemail_complete, :to => :voicemail_completed
|
57
|
+
event :customer_hangs_up, :to => :cancelled
|
63
58
|
end
|
64
59
|
|
60
|
+
state :routing do
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
65
67
|
Benefits of **CallCenter** is that it's backed by [state_machine](https://github.com/pluginaweek/state_machine). Which means you can interact with events the same you do in `state_machine`.
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
69
|
+
```ruby
|
70
|
+
@call.called!
|
71
|
+
@call.wants_voicemail!
|
72
|
+
@call.routing?
|
73
|
+
@call.render # See Rendering
|
74
|
+
```
|
71
75
|
|
72
76
|
Flow
|
73
77
|
----
|
@@ -91,9 +95,11 @@ In order to DRY up the callbacks, it is best to have use standardized callback U
|
|
91
95
|
|
92
96
|
By handling this in your controller, you can immediately retrieve the **Call** from persistence, run an event on the call, and return the rendered TwiML. Here's an example:
|
93
97
|
|
94
|
-
|
95
|
-
|
96
|
-
|
98
|
+
```ruby
|
99
|
+
def flow
|
100
|
+
render :xml => @call.run(params[:event])
|
101
|
+
end
|
102
|
+
```
|
97
103
|
|
98
104
|
For an in-depth example, take a look at [call_roulette](https://github.com/zendesk/call_roulette).
|
99
105
|
|
@@ -102,48 +108,56 @@ Rendering
|
|
102
108
|
|
103
109
|
Rendering is your way of interacting with Twilio. Thus, it provides two facilities: access to an XML builder and access to your call.
|
104
110
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
111
|
+
```ruby
|
112
|
+
state :sales do
|
113
|
+
response do |xml_builder, the_call|
|
114
|
+
xml_builder.Say "This is #{the_call.agent.name}!" # Or agent.name, you can access he call implicitly
|
115
|
+
end
|
116
|
+
end
|
117
|
+
```
|
110
118
|
|
111
119
|
Renders with `@call.render` if the current state is :sales:
|
112
120
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
121
|
+
```xml
|
122
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
123
|
+
<Response>
|
124
|
+
<Say>This is Henry!</Say>
|
125
|
+
</Response>
|
126
|
+
```
|
117
127
|
|
118
128
|
Callbacks
|
119
129
|
---------
|
120
130
|
|
121
131
|
You have control over what you want to happen before/after state transitions:
|
122
132
|
|
123
|
-
|
124
|
-
|
125
|
-
|
133
|
+
```ruby
|
134
|
+
state :voicemail do
|
135
|
+
before(:always) { # Invokes before any transition }
|
136
|
+
before(:always, :uniq => true) { # Invokes before transitions to a different state }
|
126
137
|
|
127
|
-
|
128
|
-
|
129
|
-
|
138
|
+
after(:always) { # Invokes after any transition }
|
139
|
+
after(:success) { # Invokes after any successful transition }
|
140
|
+
after(:failure) { # Invokes after any failed transition (those not covered in your call flow) }
|
130
141
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
142
|
+
after(:always, :uniq => true) { # Invokes after any transition to a different state }
|
143
|
+
after(:success, :uniq => true) { # Successful unique transitions }
|
144
|
+
after(:failure, :uniq => true) { # Failed unique transitions }
|
145
|
+
end
|
146
|
+
```
|
135
147
|
|
136
148
|
For example,
|
137
149
|
|
138
|
-
|
139
|
-
|
150
|
+
```ruby
|
151
|
+
state :voicemail do
|
152
|
+
before(:always) { log_start_event }
|
140
153
|
|
141
|
-
|
142
|
-
|
154
|
+
after(:always) { log_end_event }
|
155
|
+
after(:failure) { notify_airbrake }
|
143
156
|
|
144
|
-
|
145
|
-
|
146
|
-
|
157
|
+
after(:success, :uniq => true) { notify_browser }
|
158
|
+
after(:failure, :uniq => true) { notify_cleanup_browser }
|
159
|
+
end
|
160
|
+
```
|
147
161
|
|
148
162
|
Motivation
|
149
163
|
----------
|
@@ -173,9 +187,11 @@ Tools
|
|
173
187
|
|
174
188
|
Should you be interested in what your call center workflow looks like, you can draw.
|
175
189
|
|
176
|
-
|
177
|
-
|
178
|
-
|
190
|
+
```ruby
|
191
|
+
Call.state_machines[:status].draw(:font => 'Helvetica Neue')
|
192
|
+
# OR
|
193
|
+
@call.draw_call_flow(:font => 'Helvetica Neue')
|
194
|
+
```
|
179
195
|
|
180
196
|
Future
|
181
197
|
------
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0
|
1
|
+
0.1.0
|
data/call_center.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{call_center}
|
8
|
-
s.version = "0.0
|
8
|
+
s.version = "0.1.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Henry Hsu"]
|
12
|
-
s.date = %q{
|
12
|
+
s.date = %q{2012-06-04}
|
13
13
|
s.description = %q{Support for describing call center workflows}
|
14
14
|
s.email = %q{hhsu@zendesk.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.files = [
|
20
20
|
".document",
|
21
21
|
".rvmrc",
|
22
|
+
".travis.yml",
|
22
23
|
"Gemfile",
|
23
24
|
"Gemfile.lock",
|
24
25
|
"Guardfile",
|
@@ -30,6 +31,7 @@ Gem::Specification.new do |s|
|
|
30
31
|
"call_center.gemspec",
|
31
32
|
"init.rb",
|
32
33
|
"lib/call_center.rb",
|
34
|
+
"lib/call_center/conditional_stack.rb",
|
33
35
|
"lib/call_center/core_ext/object_instance_exec.rb",
|
34
36
|
"lib/call_center/deferred_callbacks.rb",
|
35
37
|
"lib/call_center/flow_callback.rb",
|
@@ -42,6 +44,7 @@ Gem::Specification.new do |s|
|
|
42
44
|
"test/call_center_test.rb",
|
43
45
|
"test/core_ext_test.rb",
|
44
46
|
"test/examples/call.rb",
|
47
|
+
"test/examples/dynamic_transition_call.rb",
|
45
48
|
"test/examples/legacy_call.rb",
|
46
49
|
"test/examples/multiple_flow_call.rb",
|
47
50
|
"test/examples/non_standard_call.rb",
|
data/lib/call_center.rb
CHANGED
@@ -0,0 +1,51 @@
|
|
1
|
+
module CallCenter
|
2
|
+
class ConditionalStack
|
3
|
+
def initialize
|
4
|
+
@stack = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def <<(obj)
|
8
|
+
@stack << obj
|
9
|
+
end
|
10
|
+
|
11
|
+
def pop
|
12
|
+
@stack.pop
|
13
|
+
end
|
14
|
+
|
15
|
+
def any?
|
16
|
+
@stack.any?
|
17
|
+
end
|
18
|
+
|
19
|
+
def inject(options)
|
20
|
+
current_stack = @stack.dup
|
21
|
+
options.merge(:if => lambda { |model|
|
22
|
+
current_stack.map { |conditional| conditional.evaluate(model) }.all?
|
23
|
+
})
|
24
|
+
end
|
25
|
+
|
26
|
+
class Conditional
|
27
|
+
attr_reader :name
|
28
|
+
|
29
|
+
def initialize(name)
|
30
|
+
@name = name
|
31
|
+
end
|
32
|
+
|
33
|
+
def evaluate(model)
|
34
|
+
result = model.send(@name)
|
35
|
+
if? ? result : !result
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class IfConditional < Conditional
|
40
|
+
def if?
|
41
|
+
true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class UnlessConditional < Conditional
|
46
|
+
def if?
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -65,6 +65,8 @@ end
|
|
65
65
|
|
66
66
|
# Extension for StateMachine::AlternateMachine to provide render blocks inside a state definition
|
67
67
|
class StateMachine::AlternateMachine
|
68
|
+
attr_accessor :flow_stacks
|
69
|
+
|
68
70
|
def response(state_name = nil, &blk)
|
69
71
|
if @from_state
|
70
72
|
@queued_sends << [[:response, @from_state], blk]
|
@@ -92,4 +94,37 @@ class StateMachine::AlternateMachine
|
|
92
94
|
end
|
93
95
|
@queued_sends << [[:flow_actors, name], blk]
|
94
96
|
end
|
97
|
+
|
98
|
+
def event_with_blocks(*args, &blk)
|
99
|
+
options = args.extract_options!
|
100
|
+
|
101
|
+
if flow_stacks && flow_stacks.any?
|
102
|
+
options = flow_stacks.inject(options)
|
103
|
+
end
|
104
|
+
|
105
|
+
event_without_blocks(*args.push(options), &blk)
|
106
|
+
end
|
107
|
+
|
108
|
+
alias_method :event_without_blocks, :event
|
109
|
+
alias_method :event, :event_with_blocks
|
110
|
+
|
111
|
+
def flow_if(conditional, &blk)
|
112
|
+
self.flow_stacks ||= CallCenter::ConditionalStack.new
|
113
|
+
begin
|
114
|
+
self.flow_stacks << CallCenter::ConditionalStack::IfConditional.new(conditional)
|
115
|
+
yield if block_given?
|
116
|
+
ensure
|
117
|
+
self.flow_stacks.pop
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def flow_unless(conditional, &blk)
|
122
|
+
self.flow_stacks ||= CallCenter::ConditionalStack.new
|
123
|
+
begin
|
124
|
+
self.flow_stacks << CallCenter::ConditionalStack::UnlessConditional.new(conditional)
|
125
|
+
yield if block_given?
|
126
|
+
ensure
|
127
|
+
self.flow_stacks.pop
|
128
|
+
end
|
129
|
+
end
|
95
130
|
end
|
data/test/call_center_test.rb
CHANGED
@@ -6,10 +6,92 @@ require 'test/examples/legacy_call'
|
|
6
6
|
require 'test/examples/call'
|
7
7
|
require 'test/examples/non_standard_call'
|
8
8
|
require 'test/examples/multiple_flow_call'
|
9
|
+
require 'test/examples/dynamic_transition_call'
|
9
10
|
|
10
11
|
class CallCenterTest < Test::Unit::TestCase
|
11
12
|
include CallCenter::Test::DSL
|
12
13
|
|
14
|
+
context "dynamic transition workflow" do
|
15
|
+
setup do
|
16
|
+
@call = DynamicTransitionCall.new
|
17
|
+
@call.stubs(:notify)
|
18
|
+
end
|
19
|
+
|
20
|
+
context "agents available on phone" do
|
21
|
+
setup do
|
22
|
+
@call.stubs(:agents_available?).returns(true)
|
23
|
+
@call.stubs(:via_phone?).returns(true)
|
24
|
+
end
|
25
|
+
|
26
|
+
should "transition to routing on phone" do
|
27
|
+
@call.incoming_call!
|
28
|
+
assert_equal 'routing_on_phone', @call.state
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "agents available on client" do
|
33
|
+
setup do
|
34
|
+
@call.stubs(:agents_available?).returns(true)
|
35
|
+
@call.stubs(:via_phone?).returns(false)
|
36
|
+
end
|
37
|
+
|
38
|
+
should "transition to routing on client" do
|
39
|
+
@call.incoming_call!
|
40
|
+
assert_equal 'routing_on_client', @call.state
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "no agents available" do
|
45
|
+
setup do
|
46
|
+
@call.stubs(:agents_available?).returns(false)
|
47
|
+
@call.stubs(:via_phone?).returns(true)
|
48
|
+
@call.stubs(:voicemail_full?).returns(false)
|
49
|
+
end
|
50
|
+
|
51
|
+
should "transition to voicemail" do
|
52
|
+
@call.incoming_call!
|
53
|
+
assert_equal 'voicemail', @call.state
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context "no agents available when voicemail is full" do
|
58
|
+
setup do
|
59
|
+
@call.stubs(:agents_available?).returns(false)
|
60
|
+
@call.stubs(:via_phone?).returns(false) # Doesn't matter
|
61
|
+
@call.stubs(:voicemail_full?).returns(true)
|
62
|
+
end
|
63
|
+
|
64
|
+
should "transition to voicemail" do
|
65
|
+
@call.incoming_call!
|
66
|
+
assert_equal 'cancelled', @call.state
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "out of area" do
|
71
|
+
setup do
|
72
|
+
@call.state = 'routing_on_client'
|
73
|
+
@call.stubs(:out_of_area?).returns(true)
|
74
|
+
end
|
75
|
+
|
76
|
+
should "transition to cancelled" do
|
77
|
+
@call.picks_up!
|
78
|
+
assert_equal 'cancelled', @call.state
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "not out of area" do
|
83
|
+
setup do
|
84
|
+
@call.state = 'routing_on_client'
|
85
|
+
@call.stubs(:out_of_area?).returns(false)
|
86
|
+
end
|
87
|
+
|
88
|
+
should "transition to in_conference" do
|
89
|
+
@call.picks_up!
|
90
|
+
assert_equal 'in_conference', @call.state
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
13
95
|
[:call, :legacy_call].each do |call_type|
|
14
96
|
context "#{call_type.to_s.gsub('_', ' ')} workflow" do
|
15
97
|
setup do
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class DynamicTransitionCall
|
2
|
+
include CallCenter
|
3
|
+
include CommonCallMethods
|
4
|
+
|
5
|
+
call_flow :state, :initial => :initial do
|
6
|
+
state :initial do
|
7
|
+
response do |x|
|
8
|
+
x.Say "Hello World"
|
9
|
+
end
|
10
|
+
|
11
|
+
flow_if :agents_available? do
|
12
|
+
flow_if :via_phone? do
|
13
|
+
event :incoming_call, :to => :routing_on_phone
|
14
|
+
end
|
15
|
+
|
16
|
+
flow_unless :via_phone? do
|
17
|
+
event :incoming_call, :to => :routing_on_client
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
flow_unless :agents_available? do
|
22
|
+
flow_unless :voicemail_full? do
|
23
|
+
event :incoming_call, :to => :voicemail
|
24
|
+
end
|
25
|
+
|
26
|
+
flow_if :voicemail_full? do
|
27
|
+
event :incoming_call, :to => :cancelled
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
state :routing_on_client do
|
33
|
+
flow_if :out_of_area? do
|
34
|
+
event :picks_up, :to => :cancelled
|
35
|
+
end
|
36
|
+
flow_unless :out_of_area? do
|
37
|
+
event :picks_up, :to => :in_conference
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: call_center
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 0.0.9
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Henry Hsu
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date:
|
18
|
+
date: 2012-06-04 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -264,6 +264,7 @@ extra_rdoc_files:
|
|
264
264
|
files:
|
265
265
|
- .document
|
266
266
|
- .rvmrc
|
267
|
+
- .travis.yml
|
267
268
|
- Gemfile
|
268
269
|
- Gemfile.lock
|
269
270
|
- Guardfile
|
@@ -275,6 +276,7 @@ files:
|
|
275
276
|
- call_center.gemspec
|
276
277
|
- init.rb
|
277
278
|
- lib/call_center.rb
|
279
|
+
- lib/call_center/conditional_stack.rb
|
278
280
|
- lib/call_center/core_ext/object_instance_exec.rb
|
279
281
|
- lib/call_center/deferred_callbacks.rb
|
280
282
|
- lib/call_center/flow_callback.rb
|
@@ -287,6 +289,7 @@ files:
|
|
287
289
|
- test/call_center_test.rb
|
288
290
|
- test/core_ext_test.rb
|
289
291
|
- test/examples/call.rb
|
292
|
+
- test/examples/dynamic_transition_call.rb
|
290
293
|
- test/examples/legacy_call.rb
|
291
294
|
- test/examples/multiple_flow_call.rb
|
292
295
|
- test/examples/non_standard_call.rb
|