Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
19 changes: 19 additions & 0 deletions Call-Automation-Appointment-Remainder/CallConfiguration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#from EventHandler.EventAuthHandler import EventAuthHandler


class CallConfiguration:

def __init__(self, connection_string, source_identity, source_phone_number, app_base_url, audio_file_name,Event_CallBack_Route,Appointment_Confirmed_Audio,
Appointment_Cancelled_Audio,Timed_out_Audio,Invalid_Input_Audio):
self.connection_string: str = str(connection_string)
self.source_identity: str = str(source_identity)
self.source_phone_number: str = str(source_phone_number)
self.app_base_url: str = str(app_base_url)
self.audio_file_name: str = str(audio_file_name)
self.Appointment_Confirmed_Audio: str = str(Appointment_Confirmed_Audio)
self.Appointment_Cancelled_Audio: str = str(Appointment_Cancelled_Audio)
self.Timed_out_Audio: str = str(Timed_out_Audio)
self.Invalid_Input_Audio: str = str(Invalid_Input_Audio)
self.Event_CallBack_Route:str = str(Event_CallBack_Route)
self.app_callback_url: str = app_base_url + Event_CallBack_Route
self.audio_file_url: str = audio_file_name
22 changes: 22 additions & 0 deletions Call-Automation-Appointment-Remainder/ConfigurationManager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import configparser
class ConfigurationManager:
__configuration = None
__instance = None
_configpath=None

def __init__(self):
if(self.__configuration == None):
self.__configuration = configparser.ConfigParser()
self.__configuration.read('config.ini')

@staticmethod
def get_instance():

if(ConfigurationManager.__instance == None):
ConfigurationManager.__instance = ConfigurationManager()
return ConfigurationManager.__instance

def get_app_settings(self, key):
if (key != None):
return self.__configuration.get("DEFAULT", key)
return None
12 changes: 12 additions & 0 deletions Call-Automation-Appointment-Remainder/Logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import enum


class Logger(enum.Enum):

INFORMATION = 1
ERROR = 2

@staticmethod
def log_message(message_type, message):
log_message = message_type.name + " : " + message
print(log_message)
32 changes: 32 additions & 0 deletions Call-Automation-Appointment-Remainder/config.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# app settings
[DEFAULT]
#NgrokExePath = C:/
Connectionstring=%Connectionstring%
#<!-- Phone number provisioned for the resource (in E.164 Format, e.g. +14251002000). -->
SourcePhone=%SourcePhone%
#<!-- Destination identities to call. Multiple outbound calls are seperated by a semi-colon and participants in an outbound call are seperated by a coma.
#For e.g. +14251002000, 8:acs:ab12b0ea-85ea-4f83-b0b6-84d90209c7c4_00000009-bce0-da09-54b7-xxxxxxxxxxxx; +14251002001, 8:acs:ab12b0ea-85ea-4f83-b0b6-84d90209c7c4_00000009-bce0-da09-555-xxxxxxxxxxxx). -->
targetIdentity=%targetIdentity%


EventCallBackRoute= /api/callbacks
AppBaseUri= %AppBaseUri%
AppointmentReminderMenuAudio= /Audio/AppointmentReminderMenu.wav
# AddParticipantNumber= +918374734062
AppointmentConfirmedAudio= /Audio/AppointmentConfirmedAudio.wav
AppointmentCancelledAudio= /Audio/AppointmentCancelledAudio.wav
AgentAudio= /Audio/AgentAudio.wav
InvalidInputAudio= /Audio/InvalidInputAudio.wav
TimedoutAudio = /Audio/TimedoutAudio.wav
# AddParticipant= /audio/AddParticipant.wav
# RemoveParticipant= /audio/RemoveParticipant.wav


#1= Hangup for eveyone after adding participant
#2= Hangup CA after adding participant
#3= Remove addedd participants after adding them
#HangUpScenarios= 1




211 changes: 211 additions & 0 deletions Call-Automation-Appointment-Remainder/program.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import asyncio
import re
from typing import Self
import nest_asyncio
import uuid
import azure
import azure.communication
from azure.core.messaging import CloudEvent
from azure.communication.identity._shared.models import CommunicationIdentifier,PhoneNumberIdentifier,\
CommunicationUserIdentifier,CommunicationIdentifierKind
from azure.cognitiveservices.speech import AudioDataStream, SpeechConfig, SpeechSynthesizer, SpeechSynthesisOutputFormat
import json
from aiohttp import web
from Logger import Logger
from ConfigurationManager import ConfigurationManager
from CallConfiguration import CallConfiguration
from azure.communication.identity import CommunicationIdentityClient
from azure.communication.callautomation import CallAutomationClient,CallInvite,\
CallAutomationEventParser,CallConnected,CallMediaRecognizeOptions,CallMediaRecognizeDtmfOptions,\
CallConnectionClient,CallDisconnected,PlaySource,FileSource,ParticipantsUpdated,DtmfTone,\
RecognizeCanceled,RecognizeCompleted,RecognizeFailed,AddParticipantFailed,AddParticipantSucceeded,\
PlayCompleted,PlayFailed,RemoveParticipantSucceeded,RemoveParticipantFailed


class Program():

Target_number = None
ngrok_url = None
call_configuration: CallConfiguration = None
calling_Automation_client: CallAutomationClient = None
call_connection: CallConnectionClient = None
configuration_manager = None
user_identity_regex: str = '8:acs:[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{12}_[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{12}'
phone_identity_regex: str = '^\\+\\d{10,14}$'

def get_identifier_kind(self, participantnumber: str):
# checks the identity type returns as string
if(re.search(self.user_identity_regex, participantnumber)):
return CommunicationIdentifierKind.COMMUNICATION_USER
elif(re.search(self.phone_identity_regex, participantnumber)):
return CommunicationIdentifierKind.PHONE_NUMBER
else:
return CommunicationIdentifierKind.UNKNOWN
configuration_manager = ConfigurationManager.get_instance()
calling_Automation_client = CallAutomationClient.from_connection_string(configuration_manager.get_app_settings('Connectionstring'))
ngrok_url =configuration_manager.get_app_settings('AppBaseUri')

#call_configuration = initiate_configuration(Self,ngrok_url)

def __init__(self):
Logger.log_message(Logger.INFORMATION, 'Starting ACS Sample App ')
# Get configuration properties
self.app = web.Application()

self.app.add_routes([web.post('/api/call',self.run_sample)])
self.app.add_routes([web.get('/Audio/{file_name}', self.load_file)])
self.app.add_routes([web.post('/api/callbacks',self.start_callBack)])
web.run_app(self.app, port=8080)


async def run_sample(self,request):
self.call_configuration =self.initiate_configuration(self.ngrok_url)
try:
target_Id = self.configuration_manager.get_app_settings('targetIdentity')

if(target_Id and len(target_Id)):
source_caller_id = CommunicationUserIdentifier(self.call_configuration.source_identity)
#source_caller_id_number=CommunicationIdentifier(raw_id = call_configuration.source_identity)
target_Identity = self.get_identifier_kind(target_Id)
if target_Identity == CommunicationIdentifierKind.COMMUNICATION_USER :
self.Target_number=CommunicationUserIdentifier(target_Id)
Callinvite=CallInvite(self.Target_number)
if target_Identity == CommunicationIdentifierKind.PHONE_NUMBER :
self.Target_number=PhoneNumberIdentifier(target_Id)
Callinvite=CallInvite(self.Target_number,sourceCallIdNumber=PhoneNumberIdentifier(self.call_configuration.source_phone_number))

Logger.log_message(Logger.INFORMATION,'Performing CreateCall operation')
self.call_connection_Response = CallAutomationClient.create_call(self.calling_Automation_client ,Callinvite,callback_uri=self.call_configuration.app_callback_url)
Logger.log_message(
Logger.INFORMATION, 'Call initiated with Call Leg id -- >' + self.call_connection_Response.call_connection.call_connection_id)


except Exception as ex:
Logger.log_message(
Logger.ERROR, 'Failure occured while creating/establishing the call. Exception -- > ' + str(ex))

async def start_callBack(self,request):
try:
content = await request.content.read()
event = CallAutomationEventParser.parse(content)
call_Connection = self.calling_Automation_client.get_call_connection(event.call_connection_id)
call_Connection_Media =call_Connection.get_call_media()
if event.__class__ == CallConnected:
Logger.log_message(Logger.INFORMATION,'CallConnected event received for call connection id --> '
+ event.call_connection_id)

recognize_Options = CallMediaRecognizeDtmfOptions(self.Target_number,max_tones_to_collect=1)
recognize_Options.interrupt_prompt = True
recognize_Options.inter_tone_timeout = 30
recognize_Options.initial_silence_timeout=5
File_source=FileSource(uri=(self.call_configuration.app_base_url + self.call_configuration.audio_file_url))
File_source.play_source_id= 'AppointmentReminderMenu'
recognize_Options.play_prompt = File_source
recognize_Options.operation_context= 'AppointmentReminderMenu'
call_Connection_Media.start_recognizing(recognize_Options)

if event.__class__ == RecognizeCompleted and event.operation_context == 'AppointmentReminderMenu' :
Logger.log_message(Logger.INFORMATION,'RecognizeCompleted event received for call connection id --> '+ event.call_connection_id
+'Correlation id:'+event.correlation_id)
toneDetected=event.collect_tones_result.tones[0]
if toneDetected == DtmfTone.ONE:
playSource = FileSource(uri=(self.call_configuration.app_base_url+self.call_configuration.Appointment_Confirmed_Audio))
PlayOption = call_Connection_Media.play_to_all(playSource,content='ResponseToDtmf')
elif toneDetected == DtmfTone.TWO :
playSource = FileSource(uri=(self.call_configuration.app_base_url+self.call_configuration.Appointment_Cancelled_Audio))
PlayOption = call_Connection_Media.play_to_all(playSource,content='ResponseToDtmf')
else:
playSource = FileSource(uri=(self.call_configuration.app_base_url+self.call_configuration.Invalid_Input_Audio))
call_Connection_Media.play_to_all(playSource)


if event.__class__ == RecognizeFailed and event.operation_context == 'AppointmentReminderMenu' :
Logger.log_message(Logger.INFORMATION,'Recognition timed out for call connection id --> '+ event.call_connection_id
+'Correlation id:'+event.correlation_id)
playSource = FileSource(uri=(self.call_configuration.app_base_url+self.call_configuration.Timed_out_Audio))
call_Connection_Media.play_to_all(playSource)
if event.__class__ == PlayCompleted:
Logger.log_message(Logger.INFORMATION,'PlayCompleted event received for call connection id --> '+ event.call_connection_id
+'Call Connection Properties :'+event.correlation_id)
call_Connection.hang_up(True)
if event.__class__ == PlayFailed:
Logger.log_message(Logger.INFORMATION,'PlayFailed event received for call connection id --> '+ event.call_connection_id
+'Call Connection Properties :'+event.correlation_id)
call_Connection.hang_up(True)
if event.__class__ == ParticipantsUpdated :
Logger.log_message(Logger.INFORMATION,'Participants Updated --> ')
if event.__class__ == CallDisconnected :
Logger.log_message(Logger.INFORMATION,'Call Disconnected event received for call connection id --> '
+ event.call_connection_id)

except Exception as ex:
Logger.log_message(
Logger.ERROR, 'Failed to start Audio --> ' + str(ex))


# <summary>
# Fetch configurations from App Settings and create source identity
# </summary>
# <param name='app_base_url'>The base url of the app.</param>
# <returns>The <c CallConfiguration object.</returns>

def initiate_configuration(self, app_base_url):
try:
connection_string = self.configuration_manager.get_app_settings('Connectionstring')
source_phone_number = self.configuration_manager.get_app_settings('SourcePhone')
Event_CallBack_Route=self.configuration_manager.get_app_settings('EventCallBackRoute')
source_identity = self.create_user(connection_string)
audio_file_name = self.configuration_manager.get_app_settings('AppointmentReminderMenuAudio')
Appointment_Confirmed_Audio = self.configuration_manager.get_app_settings('AppointmentConfirmedAudio')
Appointment_Cancelled_Audio = self.configuration_manager.get_app_settings('AppointmentCancelledAudio')
Timed_out_Audio = self.configuration_manager.get_app_settings('TimedoutAudio')
Invalid_Input_Audio = self.configuration_manager.get_app_settings('InvalidInputAudio')

return CallConfiguration(connection_string, source_identity, source_phone_number, app_base_url,
audio_file_name,Event_CallBack_Route,Appointment_Confirmed_Audio,
Appointment_Cancelled_Audio,Timed_out_Audio,Invalid_Input_Audio)
except Exception as ex:
Logger.log_message(
Logger.ERROR, 'Failed to CallConfiguration. Exception -- > ' + str(ex))

# <summary>
# Get .wav Audio file
# </summary>

async def on_incoming_request_async(self, request):
param = request.rel_url.query
content = await request.content.read()
return 'OK'


async def load_file(self, request):
file_name = request.match_info.get('file_name', 'Anonymous')
resp = web.FileResponse(f'Audio/{file_name}')
return resp
# web.run_app(self.app, port=8080)


def create_user(self, connection_string):
client = CommunicationIdentityClient.from_connection_string(
connection_string)
user: CommunicationIdentifier = client.create_user()
return user.properties.get('id')
# <summary>
# Delete the user
# </summary>

def delete_user(self, connection_string, source):
client = CommunicationIdentityClient.from_connection_string(
connection_string)
user = CommunicationUserIdentifier(source)
client.delete_user(user)


if __name__ == '__main__':
Program()
# nest_asyncio.apply()
# obj = Program()
# asyncio.run(obj.program())



52 changes: 52 additions & 0 deletions Call-Automation-Appointment-Remainder/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
page_type: sample
languages:
- python
products:
- azure
- azure-communication-services
---

# Outbound Reminder Call Sample

This sample application shows how the Azure Communication Services Server, Calling package can be used to build IVR related solutions. This sample makes an outbound call to a phone number or a communication identifier and plays an audio message. If the callee presses 1 (tone1), to reschedule an appointment, then the application invites a new participant and then leaves the call. If the callee presses any other key then the application ends the call. This sample application is also capable of making multiple concurrent outbound calls.
The application is a console based application build using Python 3.9.

## Getting started

### Prerequisites

- Create an Azure account with an active subscription. For details, see [Create an account for free](https://azure.microsoft.com/free/)
- [Python](https://www.python.org/downloads/) 3.9 and above
- Create an Azure Communication Services resource. For details, see [Create an Azure Communication Resource](https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource). You'll need to record your resource **connection string** for this sample.
- Get a phone number for your new Azure Communication Services resource. For details, see [Get a phone number](https://docs.microsoft.com/azure/communication-services/quickstarts/telephony-sms/get-phone-number?pivots=platform-azp)
- Download and install [Ngrok](https://www.ngrok.com/download). As the sample is run locally, Ngrok will enable the receiving of all the events.
- Download and install [Visual C++](https://support.microsoft.com/en-us/topic/the-latest-supported-visual-c-downloads-2647da03-1eea-4433-9aff-95f26a218cc0)
- (Optional) Create Azure Speech resource for generating custom message to be played by application. Follow [here](https://docs.microsoft.com/azure/cognitive-services/speech-service/overview#try-the-speech-service-for-free) to create the resource.

> Note: the samples make use of the Microsoft Cognitive Services Speech SDK. By downloading the Microsoft Cognitive Services Speech SDK, you acknowledge its license, see [Speech SDK license agreement](https://aka.ms/csspeech/license201809).

### Configuring application

- Open the config.ini file to configure the following settings

- Connection String: Azure Communication Service resource's connection string.
- Source Phone: Phone number associated with the Azure Communication Service resource.
- DestinationIdentities: Multiple sets of outbound target and Transfer target. These sets are seperated by a semi-colon, and outbound target and Transfer target in a each set are seperated by a coma.

Format: "OutboundTarget1(PhoneNumber),TransferTarget1(PhoneNumber/MRI);OutboundTarget2(PhoneNumber),TransferTarget2(PhoneNumber/MRI);OutboundTarget3(PhoneNumber),TransferTarget3(PhoneNumber/MRI)".

For e.g. "+1425XXXAAAA,8:acs:ab12b0ea-85ea-4f83-b0b6-84d90209c7c4_00000009-bce0-da09-54b7-xxxxxxxxxxxx;+1425XXXBBBB,+1425XXXCCCC"

- NgrokExePath: Folder path where ngrok.exe is insalled/saved.
- SecretPlaceholder: Secret/Password that would be part of callback and will be use to validate incoming requests.
- CognitiveServiceKey: (Optional) Cognitive service key used for generating custom message
- CognitiveServiceRegion: (Optional) Region associated with cognitive service
- CustomMessage: (Optional) Text for the custom message to be converted to speech.

### Run the Application

- Add azure communication callingserver's wheel file path in requirement.txt
- Navigate to the directory containing the requirements.txt file and use the following commands for installing all the dependencies and for running the application respectively:
- pip install -r requirements.txt
- python program.py
Loading