SlideShare a Scribd company logo
1 / 29
Simple Callcenter
Platform with PHP
Simple Callcenter
Platform with PHP
Astricon 2018, October 9th
, Orlando, FL
Morten Amundsen / @mor10am
2 / 29
About me &
●
From Norway, Working remote from Italy.
●
Been with Teleperformance Nordic since
2003.
●
Teleperformance is a contact center provider
from France with presence in 76 countries
and has over 220.000 employees.
●
Teleperformance Nordic is serving clients in
Norway, Sweden, Denmark and Finland.
3 / 29
Creating the callcenter
●
Configuring Asterisk for agents and callers
●
Create a PHP server process:
– Communicate with Asterisk (AMI)
– Communicate with Webpage (WS)
– Create report
– Keep callcenter state in Redis
●
Restore on server restart
●
Create a simple dashboard in Vue.js
– Calls, agents, connections and statistics
●
Deploy to an Ubuntu VM with Ansible
4 / 29
Overview
5 / 29
Asterisk
●
Asterisk 13 on Ubuntu 18.04 VM (universe)
●
Main config files modified/used:
– sip.conf
– http.conf
– manager.conf
– queues.conf
– extensions.conf
6 / 29
Dialplan
[public]
exten => 113,1,goto(callcenter,s,1)
exten => 011,1,goto(agent-login,s,1)
exten => 022,1,goto(agent-logout,s,1)
7 / 29
Dialplan (Caller)
[callcenter]
exten => s,1,NoOp
same => n,Answer
same => n,Set(CHANNEL(language)=en)
same => n,Set(CHANNEL(hangup_handler_push)=hangup-handler,s,1)
same => n,UserEvent(CALLER)
same => n,Queue(q-callcenter,,,,3600)
same => n,Hangup(16)
[hangup-handler]
exten => s,1,noop
same => n,UserEvent(CALLERHANGUP)
same => n,GotoIf($["${MEMBERINTERFACE}" = ""]?skip)
same => n,UnpauseQueueMember(q-callcenter,${MEMBERINTERFACE})
same => n(skip),Return
Interface of agent
if caller talked to one
8 / 29
[agent-login]
exten => s,1,noop
same => n,Answer
same => n,Set(CHANNEL(language)=en)
same => n,Set(agentid=${CHANNEL(peername)})
same => n,Set(queue-member=local/${agentid}@agent-connect)
same => n,AddQueueMember(q-callcenter,${queue-member},,,$
{agentid})
same => n,PauseQueueMember(q-callcenter,${queue-member})
same => n,UserEvent(LOGGEDIN,agentid:${agentid},member:${queue-
member})
same => n,Playback(agent-loginok)
same => n,hangup(16)
Dialplan (Agent login)
Channel peername is used as agent Id
9 / 29
[agent-logout]
exten => s,1,noop
same => n,Answer
same => n,Set(CHANNEL(language)=en)
same => n,Set(agentid=${CHANNEL(peername)})
same => n,Set(queue-member=local/${agentid}@agent-connect)
same => n,RemoveQueueMember(q-callcenter,${queue-member})
same => n,UserEvent(LOGGEDOUT,agentid:${agentid},member:${queue-
member})
same => n,Playback(agent-loggedoff)
same => n,hangup(16)
Dialplan (Agent logout)
Channel peername is used as agent Id
10 / 29
Dialplan (Bridge call)
[agent-connect]
exten => _X.,1,Dial(SIP/${EXTEN},30,r)
Agent Id is the same
as the extension.
ex.: local/1234@agent-connect
11 / 29
Demo
12 / 29
PHP Server
●
Libraries:
– Ratchet (Websocket connection)
●
http://socketo.me/
– React (Asterisk AMI TCP connection)
●
https://reactphp.org/
– PAMI (Encoding/decoding AMI messages)
●
http://marcelog.github.io/PAMI/
– Redis (storage of server state)
13 / 29
PHP Server
$loop = ReactEventLoopFactory::create();
$app = new RatchetApp(
‘callcenter.local’, // HTTP hostname clients intend to connect to
8080, // Port to listen on.
'0.0.0.0', // IP address to bind to
$loop // ReactEventLoop to bind the application to
);
$websockethandler = new CallcenterWebsocketHandler();
$app->route(
'/callcenter', // The URI the client will connect to
$websockethandler, // Your application to server for the route
['*'] // An array of hosts allowed to connect
);
14 / 29
PHP Server
$ami = new ReactStreamDuplexResourceStream(
stream_socket_client(‘tcp://callcenter.local:5038’),
$loop
);
$asteriskmanager = new CallcenterAsteriskManager($ami);
$reportwriter = new CallcenterReportFile(__DIR__."/report.csv");
$redis = new Redis();
$redis->connect(‘127.0.0.1’);
$callcenter = new CallcenterCallcenter(
$websockethandler,
$asteriskmanager,
$reportwriter,
$redis
);
15 / 29
PHP Server
$websockethandler->on('websocket.hello', [$callcenter, 'websocketHello']);
$websockethandler->on('websocket.avail', [$callcenter, 'websocketSetAgentAvail']);
$websockethandler->on('websocket.pause', [$callcenter, 'websocketSetAgentPause']);
$asteriskmanager->on('agent.loggedin', [$callcenter, 'agentLoggedIn']);
$asteriskmanager->on('agent.loggedout', [$callcenter, 'agentLoggedOut']);
$asteriskmanager->on('agent.paused', [$callcenter, 'agentPaused']);
$asteriskmanager->on('agent.avail', [$callcenter, 'agentAvail']);
$asteriskmanager->on('caller.new', [$callcenter, 'callNew']);
$asteriskmanager->on('caller.hangup', [$callcenter, 'callHangup']);
$asteriskmanager->on('caller.queued', [$callcenter, 'callQueued']);
$asteriskmanager->on('queue.connect', [$callcenter, 'callAndAgentConnected']);
$asteriskmanager->login('admin', 'password');
$app->run();
16 / 29
PHP Server Event
$websockethandler->on('websocket.avail', [$callcenter, 'websocketSetAgentAvail']);
public function websocketSetAgentAvail(CallcenterEvent $event) : void
{
$agent = $this->getOrCreateAgent(
$event->get('agentid'),
$event->get('member', null)
);
$this->ami->unpauseAgent($agent->getMember());
}
Event: websocket.avail
•
Callcenter method “websocketSetAgentAvail” get event from WebsocketHandler
•
Gets the agentid from this event
•
Get existing agent or create a new
•
Calls Asterisk Manager Interface method to unpause given agent
17 / 29
PHP Server Event
$asteriskmanager->on('caller.new', [$callcenter, 'callNew']);
public function callNew(CallcenterEvent $event) : void
{
$call = $this->getOrCreateCall($event->callerid, $event->uid);
$this->websocket->sendtoAll(
json_encode($call)."n".
$this->calcAndSerializeStats()."n"
);
}
Event: call.new
•
Callcenter method “callNew” get event from AsteriskManager
•
Create new call
•
Send info about call to all connected websocket dashboards together with updated
statistics.
18 / 29
PHP Server Event
Event: queue.connect
•
Get Call and Agent objects, and update their status to INCALL
•
Create new Connection model with Call and Agent
•
Update dashboard with Agent, Call and Connection, plus updated statistics
•
Send URL or Event to Agents computer. Not implemented.
$asteriskmanager->on('queue.connect', [$callcenter, 'callAndAgentConnected']);
public function callAndAgentConnected(CallcenterEvent $event) : void
{
$agentid = $event->get('agentid');
$calleruid = $event->get('calleruid');
$agent = $this->agents[$agentid];
$call = $this->calls[$calleruid];
$this->setCallStatus($call, 'INCALL');
$agent->setQueue($call->getQueue());
$this->setAgentStatus($agent, 'INCALL');
$conn = new Connection($call, $agent);
$this->agentcallconnections[$conn->id] = $conn;
$this->websocket->sendtoAll(
json_encode($agent)."n".json_encode($call)."n".json_encode($conn)."n".
$this->calcAndSerializeStats()."n"
);
/* !!! SEND URL OR EVENT TO AGENT DASHBOARD CRM OR OTHER SYSTEM !!! */
}
19 / 29
Report file
2018-09-17 15:11:07;AGENT;98427456;LOGGEDIN;12;
2018-09-17 15:11:11;CALL;3453439819;QUEUED;15;q-callcenter
2018-09-17 15:11:26;CALL;3453439819;ABANDON;0;q-callcenter
2018-09-17 15:11:39;CALL;3453439819;QUEUED;3;q-callcenter
2018-09-17 15:11:19;AGENT;98427456;AVAIL;23;q-callcenter
2018-09-17 15:11:42;CALL;3453439819;INCALL;12;q-callcenter
2018-09-17 15:11:54;CALL;3453439819;HANGUP;0;q-callcenter
2018-09-17 15:11:42;AGENT;98427456;INCALL;12;q-callcenter
2018-09-17 15:12:08;CALL;3453439819;QUEUED;1;q-callcenter
2018-09-17 15:12:09;CALL;3453439819;ABANDON;0;q-callcenter
2018-09-17 15:12:13;CALL;3453439819;QUEUED;2;q-callcenter
2018-09-17 15:11:54;AGENT;98427456;AVAIL;21;q-callcenter
2018-09-17 15:12:15;CALL;3453439819;INCALL;7;q-callcenter
2018-09-17 15:12:22;CALL;3453439819;HANGUP;0;q-callcenter
2018-09-17 15:12:15;AGENT;98427456;INCALL;7;q-callcenter
2018-09-17 15:12:22;AGENT;98427456;AVAIL;2;
2018-09-17 15:12:24;AGENT;98427456;PAUSED;49;
20 / 29
Dashboard
●
Built with:
– Vue.js
●
https://vuejs.org/
– Bootstrap CSS
●
https://getbootstrap.com/
– Websocket
21 / 29
Dashboard (cont.)
var app = new Vue({
el: '#app',
conn: null,
data: {
calls: {},
agents: {},
connections: {},
stats: {}
},
mounted: function() {
this.conn = new WebSocket('ws://callcenter.local:8080/callcenter');
this.conn.onopen = function (e) {
this.send('HELLO');
};
this.conn.onmessage = function (e) {
let lines = e.data.split("n");
lines.forEach(function (val, index) {
if (val.length) {
this.updateUI(val);
}
}, app)
};
},
22 / 29
Dashboard (cont.)
<div id="app">
<div class="row">
<stats :stats="stats" />
</div>
<div class="row">
<div class="col-sm">
<h3>Incoming calls</h3>
<call v-for="call in calls" :call="call" :key="call.id"/>
</div>
<div class="col-sm">
<h3>Agents</h3>
<agent v-for="agent in agents" :agent="agent" :key="agent.agentid" />
</div>
<div class="col-sm">
<h3>Connected calls</h3>
<connection v-for="connection in
connections" :connection="connection" :key="connection.id"/>
</div>
</div>
</div>
23 / 29
Dashboard (cont.)
<call v-for="call in calls" :call="call" :key="call.id"/>
Vue.component('call', {
props: {
call: Object
},
template: '#call-template'
});
<script type="text/x-template" id="call-template">
<div class="row">
<div class="col-sm">
<div class="alert alert-info">{{ call.callerid }} {{ call.status }}<span
class="badge badge-secondary">{{ call.queue }}</span></div>
</div>
</div>
</script>
24 / 29
Dashboard (cont.)
updateUI(msg) {
let obj = JSON.parse(msg);
if (!obj) return;
switch (obj.type) {
case 'AGENT':
this.addOrUpdateAgent(obj);
break;
case 'CALL':
this.addOrUpdateCall(obj);
break;
case 'CONNECT':
this.addOrUpdateConnection(obj);
break;
case 'STATS':
this.stats = obj;
break;
default:
break;
}
}
}
});
25 / 29
Dashboard (cont.)
addOrUpdateCall(call)
{
if (call.status == 'HANGUP' || call.status == 'ABANDON') {
this.$delete(this.calls, call.id);
// connection has same Id as call
this.$delete(this.connections, call.id);
} else {
this.$set(this.calls, call.id, call);
}
},
addOrUpdateAgent(agent)
{
if (agent.status == 'LOGGEDOUT') {
this.$delete(this.agents, agent.agentid);
} else {
this.$set(this.agents, agent.agentid, agent);
}
},
addOrUpdateConnection(connection)
{
this.$set(this.connections, connection.id, connection);
}
26 / 29
Dashboard (cont.)
{
"type": "CALL",
"id": "1536414212.52",
"callerid": "98427456",
"status": "QUEUED",
"queue": "q-callcenter",
"time": 1536414212,
"answered": 0
}
{
"type": "AGENT",
"agentid": "10092018",
"status": "AVAIL",
"queue": "",
"time": 1536414159
}
{
"type": "CONNECT",
"id": "1536414129.44",
"queue": "q-callcenter",
"time": 1536414174,
"agent": {
"type": "AGENT",
"agentid": "10092018",
"status": "INCALL",
"queue": "q-callcenter",
"time": 1536414174
},
"call": {
"type": "CALL",
"id": "1536414129.44",
"callerid": "98427456",
"status": "INCALL",
"queue": "q-callcenter",
"time": 1536414174,
"answered": 1
}
}
{
"type": "STATS",
"calls_received": 8,
"calls_abandoned": 1,
"calls_answered": 7,
"agents_online": 1,
"calls_online": 0,
"connections_online": 0,
"average_handle_time": 11,
"average_queue_time": 9,
"average_abandoned_time": 6,
"sla": 71.43
}
Messages sent over Websocket from PHP server to dashboard:
27 / 29
Ansible deployment
●
Install PHP and Asterisk
●
Copy files in place
– Asterisk configuration files
– PHP sources
– HTML and JS
●
Reload Asterisk
●
Start CTI server
28 / 29
Ansible deployment (cont.)
$> ansible-playbook ansible/playbook.yml -i ansible/inventories/production
inventories/production:
[asterisk]
callcenter.local
Playbook.yml:
- hosts: asterisk
remote_user: deploy
become: yes
roles:
- ansible-role-redis
...
- name: copy asterisk etc folder to project
synchronize:
src: asterisk/etc/
dest: /etc/asterisk/
notify:
- reload asterisk
handlers:
- name: reload asterisk
shell: asterisk -rx'core reload'
29 / 29
Thank you!
Follow us
/company/teleperformance
/teleperformanceglobal
@teleperformance
@Teleperformance_group
/teleperformance
blog.Teleperformance.com
Contact me at morten.amundsen@teleperformance.com
or @mor10am on Twitter.
Sourcecode and slides at https://github.com/mor10am/callcenter

More Related Content

PDF
Introduction of amazon connect
ODP
Phpconf 2013 - Agile Telephony Applications with PAMI and PAGI
PPTX
Web Servers(IIS, NGINX, APACHE)
PDF
Clean architectures with fast api pycones
PPTX
Introduction to Github action Presentation
PPTX
Django PPT.pptx
PDF
Web develop in flask
PDF
mastering the curl command line.pdf
Introduction of amazon connect
Phpconf 2013 - Agile Telephony Applications with PAMI and PAGI
Web Servers(IIS, NGINX, APACHE)
Clean architectures with fast api pycones
Introduction to Github action Presentation
Django PPT.pptx
Web develop in flask
mastering the curl command line.pdf

What's hot (20)

PDF
Asterisk, HTML5 and NodeJS; a world of endless possibilities
ODP
Expanding Asterisk with Kamailio
PDF
Introduction to Vagrant
PDF
Why and How to Run Your Own Gitlab Runners as Your Company Grows
PPTX
Nginx A High Performance Load Balancer, Web Server & Reverse Proxy
PPTX
Portainer
PDF
[온라인교육시리즈] 네이버 클라우드 플랫폼 init script 활용법 소개(정낙수 클라우드 솔루션 아키텍트)
PPTX
Web assembly - Future of the Web
PDF
HA Deployment Architecture with HAProxy and Keepalived
PPTX
HAProxy
PPTX
Implementation Lessons using WebRTC in Asterisk
PDF
LoadBalancer using KeepAlived
PDF
Zabbix Performance Tuning
PDF
Using Kamailio for Scalability and Security
PDF
netfilter and iptables
PDF
Linux PV on HVM
PPT
JavaScript Event Loop
PDF
IBM Notes Traveler administration and Log troubleshooting tips
PDF
Docker and WASM
PDF
DNSTap Webinar
Asterisk, HTML5 and NodeJS; a world of endless possibilities
Expanding Asterisk with Kamailio
Introduction to Vagrant
Why and How to Run Your Own Gitlab Runners as Your Company Grows
Nginx A High Performance Load Balancer, Web Server & Reverse Proxy
Portainer
[온라인교육시리즈] 네이버 클라우드 플랫폼 init script 활용법 소개(정낙수 클라우드 솔루션 아키텍트)
Web assembly - Future of the Web
HA Deployment Architecture with HAProxy and Keepalived
HAProxy
Implementation Lessons using WebRTC in Asterisk
LoadBalancer using KeepAlived
Zabbix Performance Tuning
Using Kamailio for Scalability and Security
netfilter and iptables
Linux PV on HVM
JavaScript Event Loop
IBM Notes Traveler administration and Log troubleshooting tips
Docker and WASM
DNSTap Webinar
Ad

Similar to Simple callcenter platform with PHP (20)

PPTX
SIPfoundry CoLab 2013 - Web Contact Center
PDF
QueueMetrics Icon Agent Page and Elastix Integration
PDF
Astricon2006_matt_florell_PDF.pdf
PDF
PBX on a non-specialized distro
PPTX
Presentacion inConcert Allegro 2015
PDF
Elastix Call Center
PPTX
2Ring Gadgets for Cisco Finesse 3.4 (UCCX / UCCE)
PDF
Develop web APIs in PHP using middleware with Expressive (Code Europe)
PPT
02 asterisk - the future of telecommunications
PPTX
The Elastix Call Center Protocol Revealed
PDF
Contact Center fundamentals for Kids
PPTX
Advanced RingCentral API Use Cases
PDF
Asibul Ahsan(063473056)
PDF
Build powerfull and smart web applications with Symfony2
PDF
Telephony Service Development on Asterisk Platform
PDF
SITREP - Asterisk REST. The first steps are done, now what? - CommCon 2019
PPT
Astricon 2010: Scaling Asterisk installations
PDF
Twilio Voice Applications with Amazon AWS S3 and EC2
PDF
Building web APIs in PHP with Zend Expressive
PPTX
WebRTC Conference and Expo (November 2013) - Signalling Workshop
SIPfoundry CoLab 2013 - Web Contact Center
QueueMetrics Icon Agent Page and Elastix Integration
Astricon2006_matt_florell_PDF.pdf
PBX on a non-specialized distro
Presentacion inConcert Allegro 2015
Elastix Call Center
2Ring Gadgets for Cisco Finesse 3.4 (UCCX / UCCE)
Develop web APIs in PHP using middleware with Expressive (Code Europe)
02 asterisk - the future of telecommunications
The Elastix Call Center Protocol Revealed
Contact Center fundamentals for Kids
Advanced RingCentral API Use Cases
Asibul Ahsan(063473056)
Build powerfull and smart web applications with Symfony2
Telephony Service Development on Asterisk Platform
SITREP - Asterisk REST. The first steps are done, now what? - CommCon 2019
Astricon 2010: Scaling Asterisk installations
Twilio Voice Applications with Amazon AWS S3 and EC2
Building web APIs in PHP with Zend Expressive
WebRTC Conference and Expo (November 2013) - Signalling Workshop
Ad

Recently uploaded (20)

PDF
gpt5_lecture_notes_comprehensive_20250812015547.pdf
PDF
Agricultural_Statistics_at_a_Glance_2022_0.pdf
PDF
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
PDF
Unlocking AI with Model Context Protocol (MCP)
PPTX
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
PDF
Encapsulation theory and applications.pdf
PDF
Approach and Philosophy of On baking technology
PPTX
Spectroscopy.pptx food analysis technology
PDF
cuic standard and advanced reporting.pdf
PDF
Accuracy of neural networks in brain wave diagnosis of schizophrenia
PPT
“AI and Expert System Decision Support & Business Intelligence Systems”
PDF
Getting Started with Data Integration: FME Form 101
PDF
Dropbox Q2 2025 Financial Results & Investor Presentation
PPTX
SOPHOS-XG Firewall Administrator PPT.pptx
PPTX
1. Introduction to Computer Programming.pptx
PDF
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
PDF
Video forgery: An extensive analysis of inter-and intra-frame manipulation al...
PPTX
Machine Learning_overview_presentation.pptx
PDF
The Rise and Fall of 3GPP – Time for a Sabbatical?
PPTX
Digital-Transformation-Roadmap-for-Companies.pptx
gpt5_lecture_notes_comprehensive_20250812015547.pdf
Agricultural_Statistics_at_a_Glance_2022_0.pdf
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
Unlocking AI with Model Context Protocol (MCP)
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
Encapsulation theory and applications.pdf
Approach and Philosophy of On baking technology
Spectroscopy.pptx food analysis technology
cuic standard and advanced reporting.pdf
Accuracy of neural networks in brain wave diagnosis of schizophrenia
“AI and Expert System Decision Support & Business Intelligence Systems”
Getting Started with Data Integration: FME Form 101
Dropbox Q2 2025 Financial Results & Investor Presentation
SOPHOS-XG Firewall Administrator PPT.pptx
1. Introduction to Computer Programming.pptx
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
Video forgery: An extensive analysis of inter-and intra-frame manipulation al...
Machine Learning_overview_presentation.pptx
The Rise and Fall of 3GPP – Time for a Sabbatical?
Digital-Transformation-Roadmap-for-Companies.pptx

Simple callcenter platform with PHP

  • 1. 1 / 29 Simple Callcenter Platform with PHP Simple Callcenter Platform with PHP Astricon 2018, October 9th , Orlando, FL Morten Amundsen / @mor10am
  • 2. 2 / 29 About me & ● From Norway, Working remote from Italy. ● Been with Teleperformance Nordic since 2003. ● Teleperformance is a contact center provider from France with presence in 76 countries and has over 220.000 employees. ● Teleperformance Nordic is serving clients in Norway, Sweden, Denmark and Finland.
  • 3. 3 / 29 Creating the callcenter ● Configuring Asterisk for agents and callers ● Create a PHP server process: – Communicate with Asterisk (AMI) – Communicate with Webpage (WS) – Create report – Keep callcenter state in Redis ● Restore on server restart ● Create a simple dashboard in Vue.js – Calls, agents, connections and statistics ● Deploy to an Ubuntu VM with Ansible
  • 5. 5 / 29 Asterisk ● Asterisk 13 on Ubuntu 18.04 VM (universe) ● Main config files modified/used: – sip.conf – http.conf – manager.conf – queues.conf – extensions.conf
  • 6. 6 / 29 Dialplan [public] exten => 113,1,goto(callcenter,s,1) exten => 011,1,goto(agent-login,s,1) exten => 022,1,goto(agent-logout,s,1)
  • 7. 7 / 29 Dialplan (Caller) [callcenter] exten => s,1,NoOp same => n,Answer same => n,Set(CHANNEL(language)=en) same => n,Set(CHANNEL(hangup_handler_push)=hangup-handler,s,1) same => n,UserEvent(CALLER) same => n,Queue(q-callcenter,,,,3600) same => n,Hangup(16) [hangup-handler] exten => s,1,noop same => n,UserEvent(CALLERHANGUP) same => n,GotoIf($["${MEMBERINTERFACE}" = ""]?skip) same => n,UnpauseQueueMember(q-callcenter,${MEMBERINTERFACE}) same => n(skip),Return Interface of agent if caller talked to one
  • 8. 8 / 29 [agent-login] exten => s,1,noop same => n,Answer same => n,Set(CHANNEL(language)=en) same => n,Set(agentid=${CHANNEL(peername)}) same => n,Set(queue-member=local/${agentid}@agent-connect) same => n,AddQueueMember(q-callcenter,${queue-member},,,$ {agentid}) same => n,PauseQueueMember(q-callcenter,${queue-member}) same => n,UserEvent(LOGGEDIN,agentid:${agentid},member:${queue- member}) same => n,Playback(agent-loginok) same => n,hangup(16) Dialplan (Agent login) Channel peername is used as agent Id
  • 9. 9 / 29 [agent-logout] exten => s,1,noop same => n,Answer same => n,Set(CHANNEL(language)=en) same => n,Set(agentid=${CHANNEL(peername)}) same => n,Set(queue-member=local/${agentid}@agent-connect) same => n,RemoveQueueMember(q-callcenter,${queue-member}) same => n,UserEvent(LOGGEDOUT,agentid:${agentid},member:${queue- member}) same => n,Playback(agent-loggedoff) same => n,hangup(16) Dialplan (Agent logout) Channel peername is used as agent Id
  • 10. 10 / 29 Dialplan (Bridge call) [agent-connect] exten => _X.,1,Dial(SIP/${EXTEN},30,r) Agent Id is the same as the extension. ex.: local/1234@agent-connect
  • 12. 12 / 29 PHP Server ● Libraries: – Ratchet (Websocket connection) ● http://socketo.me/ – React (Asterisk AMI TCP connection) ● https://reactphp.org/ – PAMI (Encoding/decoding AMI messages) ● http://marcelog.github.io/PAMI/ – Redis (storage of server state)
  • 13. 13 / 29 PHP Server $loop = ReactEventLoopFactory::create(); $app = new RatchetApp( ‘callcenter.local’, // HTTP hostname clients intend to connect to 8080, // Port to listen on. '0.0.0.0', // IP address to bind to $loop // ReactEventLoop to bind the application to ); $websockethandler = new CallcenterWebsocketHandler(); $app->route( '/callcenter', // The URI the client will connect to $websockethandler, // Your application to server for the route ['*'] // An array of hosts allowed to connect );
  • 14. 14 / 29 PHP Server $ami = new ReactStreamDuplexResourceStream( stream_socket_client(‘tcp://callcenter.local:5038’), $loop ); $asteriskmanager = new CallcenterAsteriskManager($ami); $reportwriter = new CallcenterReportFile(__DIR__."/report.csv"); $redis = new Redis(); $redis->connect(‘127.0.0.1’); $callcenter = new CallcenterCallcenter( $websockethandler, $asteriskmanager, $reportwriter, $redis );
  • 15. 15 / 29 PHP Server $websockethandler->on('websocket.hello', [$callcenter, 'websocketHello']); $websockethandler->on('websocket.avail', [$callcenter, 'websocketSetAgentAvail']); $websockethandler->on('websocket.pause', [$callcenter, 'websocketSetAgentPause']); $asteriskmanager->on('agent.loggedin', [$callcenter, 'agentLoggedIn']); $asteriskmanager->on('agent.loggedout', [$callcenter, 'agentLoggedOut']); $asteriskmanager->on('agent.paused', [$callcenter, 'agentPaused']); $asteriskmanager->on('agent.avail', [$callcenter, 'agentAvail']); $asteriskmanager->on('caller.new', [$callcenter, 'callNew']); $asteriskmanager->on('caller.hangup', [$callcenter, 'callHangup']); $asteriskmanager->on('caller.queued', [$callcenter, 'callQueued']); $asteriskmanager->on('queue.connect', [$callcenter, 'callAndAgentConnected']); $asteriskmanager->login('admin', 'password'); $app->run();
  • 16. 16 / 29 PHP Server Event $websockethandler->on('websocket.avail', [$callcenter, 'websocketSetAgentAvail']); public function websocketSetAgentAvail(CallcenterEvent $event) : void { $agent = $this->getOrCreateAgent( $event->get('agentid'), $event->get('member', null) ); $this->ami->unpauseAgent($agent->getMember()); } Event: websocket.avail • Callcenter method “websocketSetAgentAvail” get event from WebsocketHandler • Gets the agentid from this event • Get existing agent or create a new • Calls Asterisk Manager Interface method to unpause given agent
  • 17. 17 / 29 PHP Server Event $asteriskmanager->on('caller.new', [$callcenter, 'callNew']); public function callNew(CallcenterEvent $event) : void { $call = $this->getOrCreateCall($event->callerid, $event->uid); $this->websocket->sendtoAll( json_encode($call)."n". $this->calcAndSerializeStats()."n" ); } Event: call.new • Callcenter method “callNew” get event from AsteriskManager • Create new call • Send info about call to all connected websocket dashboards together with updated statistics.
  • 18. 18 / 29 PHP Server Event Event: queue.connect • Get Call and Agent objects, and update their status to INCALL • Create new Connection model with Call and Agent • Update dashboard with Agent, Call and Connection, plus updated statistics • Send URL or Event to Agents computer. Not implemented. $asteriskmanager->on('queue.connect', [$callcenter, 'callAndAgentConnected']); public function callAndAgentConnected(CallcenterEvent $event) : void { $agentid = $event->get('agentid'); $calleruid = $event->get('calleruid'); $agent = $this->agents[$agentid]; $call = $this->calls[$calleruid]; $this->setCallStatus($call, 'INCALL'); $agent->setQueue($call->getQueue()); $this->setAgentStatus($agent, 'INCALL'); $conn = new Connection($call, $agent); $this->agentcallconnections[$conn->id] = $conn; $this->websocket->sendtoAll( json_encode($agent)."n".json_encode($call)."n".json_encode($conn)."n". $this->calcAndSerializeStats()."n" ); /* !!! SEND URL OR EVENT TO AGENT DASHBOARD CRM OR OTHER SYSTEM !!! */ }
  • 19. 19 / 29 Report file 2018-09-17 15:11:07;AGENT;98427456;LOGGEDIN;12; 2018-09-17 15:11:11;CALL;3453439819;QUEUED;15;q-callcenter 2018-09-17 15:11:26;CALL;3453439819;ABANDON;0;q-callcenter 2018-09-17 15:11:39;CALL;3453439819;QUEUED;3;q-callcenter 2018-09-17 15:11:19;AGENT;98427456;AVAIL;23;q-callcenter 2018-09-17 15:11:42;CALL;3453439819;INCALL;12;q-callcenter 2018-09-17 15:11:54;CALL;3453439819;HANGUP;0;q-callcenter 2018-09-17 15:11:42;AGENT;98427456;INCALL;12;q-callcenter 2018-09-17 15:12:08;CALL;3453439819;QUEUED;1;q-callcenter 2018-09-17 15:12:09;CALL;3453439819;ABANDON;0;q-callcenter 2018-09-17 15:12:13;CALL;3453439819;QUEUED;2;q-callcenter 2018-09-17 15:11:54;AGENT;98427456;AVAIL;21;q-callcenter 2018-09-17 15:12:15;CALL;3453439819;INCALL;7;q-callcenter 2018-09-17 15:12:22;CALL;3453439819;HANGUP;0;q-callcenter 2018-09-17 15:12:15;AGENT;98427456;INCALL;7;q-callcenter 2018-09-17 15:12:22;AGENT;98427456;AVAIL;2; 2018-09-17 15:12:24;AGENT;98427456;PAUSED;49;
  • 20. 20 / 29 Dashboard ● Built with: – Vue.js ● https://vuejs.org/ – Bootstrap CSS ● https://getbootstrap.com/ – Websocket
  • 21. 21 / 29 Dashboard (cont.) var app = new Vue({ el: '#app', conn: null, data: { calls: {}, agents: {}, connections: {}, stats: {} }, mounted: function() { this.conn = new WebSocket('ws://callcenter.local:8080/callcenter'); this.conn.onopen = function (e) { this.send('HELLO'); }; this.conn.onmessage = function (e) { let lines = e.data.split("n"); lines.forEach(function (val, index) { if (val.length) { this.updateUI(val); } }, app) }; },
  • 22. 22 / 29 Dashboard (cont.) <div id="app"> <div class="row"> <stats :stats="stats" /> </div> <div class="row"> <div class="col-sm"> <h3>Incoming calls</h3> <call v-for="call in calls" :call="call" :key="call.id"/> </div> <div class="col-sm"> <h3>Agents</h3> <agent v-for="agent in agents" :agent="agent" :key="agent.agentid" /> </div> <div class="col-sm"> <h3>Connected calls</h3> <connection v-for="connection in connections" :connection="connection" :key="connection.id"/> </div> </div> </div>
  • 23. 23 / 29 Dashboard (cont.) <call v-for="call in calls" :call="call" :key="call.id"/> Vue.component('call', { props: { call: Object }, template: '#call-template' }); <script type="text/x-template" id="call-template"> <div class="row"> <div class="col-sm"> <div class="alert alert-info">{{ call.callerid }} {{ call.status }}<span class="badge badge-secondary">{{ call.queue }}</span></div> </div> </div> </script>
  • 24. 24 / 29 Dashboard (cont.) updateUI(msg) { let obj = JSON.parse(msg); if (!obj) return; switch (obj.type) { case 'AGENT': this.addOrUpdateAgent(obj); break; case 'CALL': this.addOrUpdateCall(obj); break; case 'CONNECT': this.addOrUpdateConnection(obj); break; case 'STATS': this.stats = obj; break; default: break; } } } });
  • 25. 25 / 29 Dashboard (cont.) addOrUpdateCall(call) { if (call.status == 'HANGUP' || call.status == 'ABANDON') { this.$delete(this.calls, call.id); // connection has same Id as call this.$delete(this.connections, call.id); } else { this.$set(this.calls, call.id, call); } }, addOrUpdateAgent(agent) { if (agent.status == 'LOGGEDOUT') { this.$delete(this.agents, agent.agentid); } else { this.$set(this.agents, agent.agentid, agent); } }, addOrUpdateConnection(connection) { this.$set(this.connections, connection.id, connection); }
  • 26. 26 / 29 Dashboard (cont.) { "type": "CALL", "id": "1536414212.52", "callerid": "98427456", "status": "QUEUED", "queue": "q-callcenter", "time": 1536414212, "answered": 0 } { "type": "AGENT", "agentid": "10092018", "status": "AVAIL", "queue": "", "time": 1536414159 } { "type": "CONNECT", "id": "1536414129.44", "queue": "q-callcenter", "time": 1536414174, "agent": { "type": "AGENT", "agentid": "10092018", "status": "INCALL", "queue": "q-callcenter", "time": 1536414174 }, "call": { "type": "CALL", "id": "1536414129.44", "callerid": "98427456", "status": "INCALL", "queue": "q-callcenter", "time": 1536414174, "answered": 1 } } { "type": "STATS", "calls_received": 8, "calls_abandoned": 1, "calls_answered": 7, "agents_online": 1, "calls_online": 0, "connections_online": 0, "average_handle_time": 11, "average_queue_time": 9, "average_abandoned_time": 6, "sla": 71.43 } Messages sent over Websocket from PHP server to dashboard:
  • 27. 27 / 29 Ansible deployment ● Install PHP and Asterisk ● Copy files in place – Asterisk configuration files – PHP sources – HTML and JS ● Reload Asterisk ● Start CTI server
  • 28. 28 / 29 Ansible deployment (cont.) $> ansible-playbook ansible/playbook.yml -i ansible/inventories/production inventories/production: [asterisk] callcenter.local Playbook.yml: - hosts: asterisk remote_user: deploy become: yes roles: - ansible-role-redis ... - name: copy asterisk etc folder to project synchronize: src: asterisk/etc/ dest: /etc/asterisk/ notify: - reload asterisk handlers: - name: reload asterisk shell: asterisk -rx'core reload'
  • 29. 29 / 29 Thank you! Follow us /company/teleperformance /teleperformanceglobal @teleperformance @Teleperformance_group /teleperformance blog.Teleperformance.com Contact me at morten.amundsen@teleperformance.com or @mor10am on Twitter. Sourcecode and slides at https://github.com/mor10am/callcenter