diff --git a/callautomation-Simple-IVR/Audio/AddParticipant.wav b/callautomation-Simple-IVR/Audio/AddParticipant.wav new file mode 100644 index 0000000..7491fbd Binary files /dev/null and b/callautomation-Simple-IVR/Audio/AddParticipant.wav differ diff --git a/callautomation-Simple-IVR/Audio/InvalidInputAudio.wav b/callautomation-Simple-IVR/Audio/InvalidInputAudio.wav new file mode 100644 index 0000000..3dfa656 Binary files /dev/null and b/callautomation-Simple-IVR/Audio/InvalidInputAudio.wav differ diff --git a/callautomation-Simple-IVR/Audio/RemoveParticipant.wav b/callautomation-Simple-IVR/Audio/RemoveParticipant.wav new file mode 100644 index 0000000..9b6f577 Binary files /dev/null and b/callautomation-Simple-IVR/Audio/RemoveParticipant.wav differ diff --git a/callautomation-Simple-IVR/Audio/TimedoutAudio.wav b/callautomation-Simple-IVR/Audio/TimedoutAudio.wav new file mode 100644 index 0000000..fb76eeb Binary files /dev/null and b/callautomation-Simple-IVR/Audio/TimedoutAudio.wav differ diff --git a/callautomation-Simple-IVR/Audio/agent.wav b/callautomation-Simple-IVR/Audio/agent.wav new file mode 100644 index 0000000..522006c Binary files /dev/null and b/callautomation-Simple-IVR/Audio/agent.wav differ diff --git a/callautomation-Simple-IVR/Audio/customercare.wav b/callautomation-Simple-IVR/Audio/customercare.wav new file mode 100644 index 0000000..dc0ac9e Binary files /dev/null and b/callautomation-Simple-IVR/Audio/customercare.wav differ diff --git a/callautomation-Simple-IVR/Audio/invalid.wav b/callautomation-Simple-IVR/Audio/invalid.wav new file mode 100644 index 0000000..04e7db7 Binary files /dev/null and b/callautomation-Simple-IVR/Audio/invalid.wav differ diff --git a/callautomation-Simple-IVR/Audio/mainmenu.wav b/callautomation-Simple-IVR/Audio/mainmenu.wav new file mode 100644 index 0000000..b96672b Binary files /dev/null and b/callautomation-Simple-IVR/Audio/mainmenu.wav differ diff --git a/callautomation-Simple-IVR/Audio/marketing.wav b/callautomation-Simple-IVR/Audio/marketing.wav new file mode 100644 index 0000000..e997811 Binary files /dev/null and b/callautomation-Simple-IVR/Audio/marketing.wav differ diff --git a/callautomation-Simple-IVR/Audio/sales.wav b/callautomation-Simple-IVR/Audio/sales.wav new file mode 100644 index 0000000..dcccfab Binary files /dev/null and b/callautomation-Simple-IVR/Audio/sales.wav differ diff --git a/callautomation-Simple-IVR/ConfigurationManager.py b/callautomation-Simple-IVR/ConfigurationManager.py new file mode 100644 index 0000000..a3a82e7 --- /dev/null +++ b/callautomation-Simple-IVR/ConfigurationManager.py @@ -0,0 +1,23 @@ +import configparser + + +class ConfigurationManager: + __configuration = None + __instance = 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 + + diff --git a/callautomation-Simple-IVR/Logger.py b/callautomation-Simple-IVR/Logger.py new file mode 100644 index 0000000..3014d46 --- /dev/null +++ b/callautomation-Simple-IVR/Logger.py @@ -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) diff --git a/callautomation-Simple-IVR/__init__.py b/callautomation-Simple-IVR/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/callautomation-Simple-IVR/config.ini b/callautomation-Simple-IVR/config.ini new file mode 100644 index 0000000..434e29d --- /dev/null +++ b/callautomation-Simple-IVR/config.ini @@ -0,0 +1,16 @@ +# app settings +[default] +Connectionstring=%Connectionstring% +# +ACSAlternatePhoneNumber=%ACSAlternatePhoneNumber% +ParticipantToAdd=%ParticipantToAdd% +BaseUri=%BaseUri% +MainMenuAudio = /Audio/mainmenu.wav +SalesAudio= /Audio/sales.wav +MarketingAudio= /Audio/marketing.wav +CustomerCareAudio=/Audio/customercare.wav +AddParticipant = /Audio/AddParticipant.wav +AgentAudio= /Audio/agent.wav +InvalidInputAudio= /Audio/InvalidInputAudio.wav +TimedoutAudio = /Audio/TimedoutAudio.wav +AllowedHosts = * diff --git a/callautomation-Simple-IVR/program.py b/callautomation-Simple-IVR/program.py new file mode 100644 index 0000000..0301126 --- /dev/null +++ b/callautomation-Simple-IVR/program.py @@ -0,0 +1,177 @@ +import re +import azure +import ast +from azure.eventgrid import EventGridEvent +from azure.communication.identity._shared.models import PhoneNumberIdentifier,\ + CommunicationUserIdentifier,CommunicationIdentifierKind,identifier_from_raw_id +import json +from aiohttp import web +from Logger import Logger +from ConfigurationManager import ConfigurationManager +from azure.communication.callautomation import CallAutomationClient,CallInvite,\ +CallAutomationEventParser,CallConnected,CallMediaRecognizeDtmfOptions,\ +FileSource,DtmfTone,PlayCompleted,PlayFailed,AddParticipantSucceeded,AddParticipantFailed,\ +RecognizeCompleted,RecognizeFailed\ + +configuration_manager = ConfigurationManager.get_instance() +calling_Automation_client = CallAutomationClient.from_connection_string(configuration_manager.get_app_settings("Connectionstring")) +base_uri=configuration_manager.get_app_settings('base_uri') +caller_id = 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 + +class Program(): + + def __init__(self): + app = web.Application() + app.add_routes([web.post('/api/incomingCall', self.run_sample)]) + app.add_routes([web.get('/Audio/{file_name}', self.load_file)]) + app.add_routes([web.post('/api/calls/{contextId}', self.start_callBack)]) + web.run_app(app, port=58963) + + async def run_sample(self,request): + try: + self.source_identity=CommunicationUserIdentifier(configuration_manager.get_app_settings("Connectionstring")) + content = await request.content.read() + post_data = str(content.decode('UTF-8')) + if post_data: + json_data = ast.literal_eval(json.dumps(post_data)) + event = EventGridEvent.from_dict(ast.literal_eval(json_data)[0]) + event_data = event.data + if event.event_type == 'Microsoft.EventGrid.SubscriptionValidationEvent': + try: + subscription_validation_event = event_data + code = subscription_validation_event['validationCode'] + if code: + data = {"validationResponse": code} + Logger.log_message(Logger.INFORMATION, + "Successfully Subscribed EventGrid.ValidationEvent --> " + str(data)) + return web.Response(body=str(data), status=200) + except Exception as ex: + Logger.log_message( + Logger.ERROR, "Failed to Subscribe EventGrid.ValidationEvent --> " + str(ex)) + return web.Response(text=str(ex), status=500) + + callerId = str(event_data['from']['rawId']) + incoming_call_context = event_data['incomingCallContext'] + callback_uri = base_uri + '/api/calls/callerId=?'+ callerId + answer_call_result = calling_Automation_client.answer_call(incoming_call_context, callback_uri) + return web.Response(status=200) + except Exception as ex: + Logger.log_message( + Logger.ERROR, "Failed to start Call Connection --> " + str(ex)) + + async def load_file(self, request): + file_name = request.match_info.get('file_name', 'Anonymous') + resp = web.FileResponse(f'Audio/{file_name}') + return resp + + async def start_callBack(self,request): + try: + content = await request.content.read() + event = CallAutomationEventParser.parse(content) + Logger.log_message(Logger.INFORMATION,'Event Kind IS :' + + event.kind) + call_connection = 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(identifier_from_raw_id(request.query_string),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=(base_uri + configuration_manager.get_app_settings('MainMenuAudio'))) + File_source.play_source_id= 'MainMenu' + recognize_Options.play_prompt = File_source + recognize_Options.operation_context= 'MainMenu' + call_connection_media.start_recognizing(recognize_Options) + + if event.__class__ == RecognizeCompleted and event.operation_context == 'MainMenu' : + Logger.log_message(Logger.INFORMATION,'RecognizeCompleted event received for call connection id --> '+ event.call_connection_id + +'Correlation id:'+event.correlation_id) + tone_detected=event.collect_tones_result.tones[0] + if tone_detected == DtmfTone.ONE: + play_source = FileSource(uri=(base_uri + configuration_manager.get_app_settings('SalesAudio'))) + play_source.play_source_id='SimpleIVR' + call_connection_media.play_to_all(play_source,operation_context='SimpleIVR') + + elif tone_detected == DtmfTone.TWO : + play_source = FileSource(uri=(base_uri + configuration_manager.get_app_settings('MarketingAudio'))) + call_connection_media.play_to_all(play_source,operation_context='SimpleIVR') + elif tone_detected == DtmfTone.THREE : + play_source = FileSource(uri=(base_uri + configuration_manager.get_app_settings('CustomerCareAudio'))) + call_connection_media.play_to_all(play_source, operation_context='SimpleIVR') + elif tone_detected == DtmfTone.FOUR : + play_source = FileSource(uri=(base_uri + configuration_manager.get_app_settings('AgentAudio'))) + call_connection_media.play_to_all(play_source, operation_context='AgentConnect') + elif tone_detected == DtmfTone.FIVE : + call_connection.hang_up(True) + else: + play_source = FileSource(uri=(base_uri + configuration_manager.get_app_settings('InvalidAudio'))) + call_connection_media.play_to_all(play_source, operation_context='SimpleIVR') + + + if event.__class__ == RecognizeFailed and event.operation_context == 'MainMenu' : + Logger.log_message(Logger.INFORMATION,'Recognition timed out for call connection id --> '+ event.call_connection_id + +'Correlation id:'+event.correlation_id) + play_source = FileSource(uri=(base_uri + configuration_manager.get_app_settings('InvalidAudio'))) + call_connection_media.play_to_all(play_source,operation_context='SimpleIVR') + if event.__class__ == PlayCompleted: + + if event.operation_context == 'AgentConnect': + participant_to_add = configuration_manager.get_app_settings('ParticipantToAdd') + + if(participant_to_add and len(participant_to_add)): + + Participant_Identity = get_identifier_kind(participant_to_add) + if Participant_Identity == CommunicationIdentifierKind.COMMUNICATION_USER : + self.Participant_Add=CommunicationUserIdentifier(participant_to_add) + call_invite=CallInvite(self.Participant_Add) + if Participant_Identity == CommunicationIdentifierKind.PHONE_NUMBER : + self.Participant_Add=PhoneNumberIdentifier(participant_to_add) + call_invite=CallInvite(self.Participant_Add,sourceCallIdNumber=self.source_identity) + + Logger.log_message(Logger.INFORMATION,'Performing add Participant operation') + self.add_participant_response=call_connection.add_participant(call_invite) + Logger.log_message( + Logger.INFORMATION, 'Call initiated with Call Leg id -- >' + self.add_participant_response.participant) + + if event.operation_context == 'SimpleIVR': + 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__ == AddParticipantSucceeded: + Logger.log_message(Logger.INFORMATION,'AddParticipantSucceeded event received for call connection id --> '+ event.call_connection_id + +'Call Connection Properties :'+event.correlation_id) + call_connection.hang_up(True) + if event.__class__ == AddParticipantFailed: + Logger.log_message(Logger.INFORMATION,'AddParticipantFailed event received for call connection id --> '+ event.call_connection_id + +'Call Connection Properties :'+event.correlation_id) + call_connection.hang_up(True) + + except Exception as ex: + Logger.log_message( + Logger.ERROR, "Call objects failed to get for connection id --> " + str(ex)) + + + +if __name__ == '__main__': + Program() + + \ No newline at end of file diff --git a/callautomation-Simple-IVR/readme.md b/callautomation-Simple-IVR/readme.md new file mode 100644 index 0000000..92fb66f --- /dev/null +++ b/callautomation-Simple-IVR/readme.md @@ -0,0 +1,50 @@ +--- +page_type: sample +languages: +- python +products: +- azure +- azure-communication-CallAutomation +--- + +# Call Automation - Simple IVR Solution + +The purpose of this sample application is to demonstrate the usage of the Azure Communication Call Automation SDK for building solutions related to Interactive Voice Response (IVR). The application accepts an incoming call when an callee dialed in to either ACS Communication Identifier or ACS acquired phone number. Application prompt the Dual-Tone Multi-Frequency (DTMF) tones to select, and then plays the appropriate audio file based on the key pressed by the callee. The application has been configured to accept tone-1 through tone-5, and if any other key is pressed, the callee will hear an invalid tone and the call will be disconnected. +The application is a console based application build using Python 3.9 and above. + +## 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 [VS Code](https://code.visualstudio.com/download) or [Visual Studio (2022 v17.4.0 and above)](https://visualstudio.microsoft.com/vs/) +-[Python311](https://www.python.org/downloads/) (Make sure to install version that corresponds with your visual studio instance, 32 vs 64 bit) +- Download and install [Ngrok](https://www.ngrok.com/download). As the sample is run locally, Ngrok will enable the receiving of all the events. +- Generate Ngrok Url by using below steps. + - Open command prompt or powershell window on the machine using to run the sample. + - Navigate to directory path where Ngrok.exe file is located. Then, run: + - ngrok http {portNumber}(For e.g. ngrok http 8080) + - Get Ngrok Url generated. Ngrok Url will be in the form of e.g. "https://95b6-43-230-212-228.ngrok-free.app" + + + + +### 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. + - ParticipantToAdd: Target phone number to add as participant. + - Base Uri: Base url of the app. (For local development replace the Ngrok url.For e.g. "https://95b6-43-230-212-228.ngrok-free.app") + +### Run the Application + +- Add azure communication CallAutomation 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 diff --git a/callautomation-Simple-IVR/requirements.txt b/callautomation-Simple-IVR/requirements.txt new file mode 100644 index 0000000..1007039 --- /dev/null +++ b/callautomation-Simple-IVR/requirements.txt @@ -0,0 +1,3 @@ +aiohttp==3.7.4.post0 +azure-communication-callautomation @file:///C:/Users/v-moirf/pyenv311/azure_communication_callautomation-1.0.0a20230413001-py3-none-any.whl +azure-communication-identity==1.3.1 \ No newline at end of file