diff --git a/beta4-media-test-outbound/audio/MainMenu.wav b/beta4-media-test-outbound/audio/MainMenu.wav new file mode 100644 index 0000000..dfb6de0 Binary files /dev/null and b/beta4-media-test-outbound/audio/MainMenu.wav differ diff --git a/beta4-media-test-outbound/data/OutboundCallDesign.png b/beta4-media-test-outbound/data/OutboundCallDesign.png new file mode 100644 index 0000000..c750ab7 Binary files /dev/null and b/beta4-media-test-outbound/data/OutboundCallDesign.png differ diff --git a/beta4-media-test-outbound/main.py b/beta4-media-test-outbound/main.py new file mode 100644 index 0000000..fc2a172 --- /dev/null +++ b/beta4-media-test-outbound/main.py @@ -0,0 +1,294 @@ +from azure.eventgrid import EventGridEvent, SystemEventNames +from flask import Flask, Response, request, json, send_file, render_template, redirect +from logging import INFO +from azure.communication.callautomation import ( + CallAutomationClient, + CallConnectionClient, + PhoneNumberIdentifier, + RecognizeInputType, + MicrosoftTeamsUserIdentifier, + CallInvite, + RecognitionChoice, + DtmfTone, + TextSource, + MediaStreamingOptions, + MediaStreamingTransportType, + MediaStreamingContentType, + MediaStreamingAudioChannelType, + FileSource, + SsmlSource, + TranscriptionOptions, + TranscriptionTransportType) +from azure.core.messaging import CloudEvent +import time +# Your ACS resource connection string +ACS_CONNECTION_STRING = "" + +# Your ACS resource phone number will act as source number to start outbound call +ACS_PHONE_NUMBER = "" + +# Target phone number you want to receive the call. +TARGET_PHONE_NUMBER = "" + +# Callback events URI to handle callback events. +CALLBACK_URI_HOST = "" +CALLBACK_EVENTS_URI = CALLBACK_URI_HOST + "/api/callbacks" +COGNITIVE_SERVICES_ENDPOINT = "" + +#(OPTIONAL) Your target Microsoft Teams user Id ex. "ab01bc12-d457-4995-a27b-c405ecfe4870" +TARGET_TEAMS_USER_ID = "" + +TEMPLATE_FILES_PATH = "template" +AUDIO_FILES_PATH = "/audio" +MAIN_MENU_PROMPT_URI = CALLBACK_URI_HOST + AUDIO_FILES_PATH + "/MainMenu.wav" + +# Prompts for text to speech +SPEECH_TO_TEXT_VOICE = "en-US-NancyNeural" +MAIN_MENU = "Hello this is Contoso Bank, we’re calling in regard to your appointment tomorrow at 9am to open a new account. Please say confirm if this time is still suitable for you or say cancel if you would like to cancel this appointment." +CONFIRMED_TEXT = "Thank you for confirming your appointment tomorrow at 9am, we look forward to meeting with you." +CANCEL_TEXT = "Your appointment tomorrow at 9am has been cancelled. Please call the bank directly if you would like to rebook for another date and time." +CUSTOMER_QUERY_TIMEOUT = "I’m sorry I didn’t receive a response, please try again." +NO_RESPONSE = "I didn't receive an input, we will go ahead and confirm your appointment. Goodbye" +INVALID_AUDIO = "I’m sorry, I didn’t understand your response, please try again." +CONFIRM_CHOICE_LABEL = "Confirm" +CANCEL_CHOICE_LABEL = "Cancel" +RETRY_CONTEXT = "retry" +DTMF_TEXT = "Press 1, 2, 3, 4 on your key board!" + +call_automation_client = CallAutomationClient.from_connection_string(ACS_CONNECTION_STRING) + +app = Flask(__name__, + static_folder=AUDIO_FILES_PATH.strip("/"), + static_url_path=AUDIO_FILES_PATH, + template_folder=TEMPLATE_FILES_PATH) + +def get_choices(): + choices = [ + RecognitionChoice(label = CONFIRM_CHOICE_LABEL, phrases= ["Confirm", "First", "One"], tone = DtmfTone.ONE), + RecognitionChoice(label = CANCEL_CHOICE_LABEL, phrases= ["Cancel", "Second", "Two"], tone = DtmfTone.TWO) + ] + return choices + +def get_media_recognize_options(call_connection_client: CallConnectionClient, text_to_play: str, target_participant:str, choices: any, context: str): + play_source = TextSource (text= text_to_play, voice_name= SPEECH_TO_TEXT_VOICE) + file_source = FileSource("https://www2.cs.uic.edu/~i101/SoundFiles/StarWars3.wav") + file_source_invalid = FileSource("https://dummy/dummy.wav") + ssml_source = SsmlSource(ssml_text='SSML Prompt') + # play_prompts = [ssml_source, play_source, file_source, ssml_source, play_source, file_source,ssml_source, play_source, file_source,ssml_source] + play_prompts = [] + call_connection_client.start_recognizing_media( + input_type=RecognizeInputType.CHOICES, + target_participant=target_participant, + choices=choices, + play_prompt=play_source, + # play_prompt=play_prompts, + interrupt_prompt=False, + initial_silence_timeout=10, + operation_context="choicesContext" + ) + + # call_connection_client.start_recognizing_media( + # input_type=RecognizeInputType.DTMF, + # target_participant=target_participant, + # # play_prompt=play_source, + # play_prompt=play_prompts, + # dtmf_max_tones_to_collect=1, + # interrupt_prompt=False, + # initial_silence_timeout=10, + # operation_context="dtmfContext" + # ) + + # call_connection_client.start_recognizing_media( + # input_type=RecognizeInputType.SPEECH, + # target_participant=target_participant, + # end_silence_timeout=10, + # play_prompt=play_prompts, + # operation_context="OpenQuestionSpeech") + + # call_connection_client.start_recognizing_media( + # dtmf_max_tones_to_collect=1, + # input_type=RecognizeInputType.SPEECH_OR_DTMF, + # target_participant=target_participant, + # end_silence_timeout=10, + # play_prompt=play_prompts, + # initial_silence_timeout=30, + # interrupt_prompt=True, + # operation_context="OpenQuestionSpeechOrDtmf") + +def handle_play(call_connection_client: CallConnectionClient, text_to_play: str,context:str): + play_source = TextSource(text=text_to_play, voice_name=SPEECH_TO_TEXT_VOICE) + # play_source = FileSource("https://www2.cs.uic.edu/~i101/SoundFiles/StarWars3.wav") + # play_source = TextSource(text="Hi, This is multiple play source call media test.", voice_name=SPEECH_TO_TEXT_VOICE) + file_source = FileSource("https://www2.cs.uic.edu/~i101/SoundFiles/StarWars3.wav") + file_source_invalid = FileSource("https://dummy/dummy.wav") + ssml_source = SsmlSource(ssml_text='SSML Prompt') + # play_sources = [ssml_source, play_source, file_source, ssml_source, play_source, file_source, ssml_source, play_source, file_source,ssml_source] + play_sources = [file_source, file_source_invalid] + call_connection_client.play_media_to_all( + play_source=play_sources, + interrupt_call_media_operation=False, + operation_context="playContext", + operation_callback_url=CALLBACK_EVENTS_URI, + loop=False + ) + + # call_connection_client.play_media_to_all(play_source,operation_context=context) + + # Interrupt Play + # play_source = TextSource(text="This is interrupt call media test.", voice_name=SPEECH_TO_TEXT_VOICE) + # # play_source = FileSource("https://www2.cs.uic.edu/~i101/SoundFiles/StarWars3.wav") + # call_connection_client.play_media_to_all( + # play_source, + # interrupt_call_media_operation=False, + # operation_context="interruptContext", + # operation_callback_url=CALLBACK_EVENTS_URI, + # loop=False) + + # play_to = [PhoneNumberIdentifier(TARGET_PHONE_NUMBER)] + # call_connection_client._play_media( + # play_source=play_sources, + # play_to=play_to + # # interrupt_call_media_operation=False, + # # operation_callback_url=CALLBACK_EVENTS_URI, + # # loop=False, + # # operation_context="playContext" + # ) +# GET endpoint to place phone call +@app.route('/outboundCall') +def outbound_call_handler(): + target_participant = PhoneNumberIdentifier(TARGET_PHONE_NUMBER) + source_caller = PhoneNumberIdentifier(ACS_PHONE_NUMBER) + + call_connection_properties = call_automation_client.create_call(target_participant, + CALLBACK_EVENTS_URI, + cognitive_services_endpoint=COGNITIVE_SERVICES_ENDPOINT, + source_caller_id_number=source_caller, + # media_streaming=media_streaming_options + # transcription=transcription_options + ) + app.logger.info("Created call with connection id: %s", call_connection_properties.call_connection_id) + return redirect("/") + + +# POST endpoint to handle callback events +@app.route('/api/callbacks', methods=['POST']) +def callback_events_handler(): + for event_dict in request.json: + # Parsing callback events + event = CloudEvent.from_dict(event_dict) + call_connection_id = event.data['callConnectionId'] + correlation_id = event.data['correlationId'] + app.logger.info("%s event received for call connection id: %s", event.type, call_connection_id) + app.logger.info("%s CORRELATION ID ======>: %s", event.type, correlation_id) + call_connection_client = call_automation_client.get_call_connection(call_connection_id) + target_participant = PhoneNumberIdentifier(TARGET_PHONE_NUMBER) + if event.type == "Microsoft.Communication.CallConnected": + # (Optional) Add a Microsoft Teams user to the call. Uncomment the below snippet to enable Teams Interop scenario. + # call_connection_client.add_participant(target_participant = CallInvite( + # target = MicrosoftTeamsUserIdentifier(user_id=TARGET_TEAMS_USER_ID), + # source_display_name = "Jack (Contoso Tech Support)")) + app.logger.info("Call Connected.=%s", event.data) + app.logger.info("Starting recognize") + get_media_recognize_options( + call_connection_client=call_connection_client, + text_to_play=MAIN_MENU, + target_participant=target_participant, + choices=get_choices(),context="") + elif event.type == "Microsoft.Communication.HoldFailed": + app.logger.info("Hold Failed.") + resultInformation = event.data['resultInformation'] + app.logger.info("Encountered error during Hold, message=%s, code=%s, subCode=%s", + resultInformation['message'], + resultInformation['code'], + resultInformation['subCode']) + elif event.type == "Microsoft.Communication.PlayStarted": + app.logger.info("PlayStarted event received.") + app.logger.info("*******************************") + # Perform different actions based on DTMF tone received from RecognizeCompleted event + elif event.type == "Microsoft.Communication.RecognizeCompleted": + app.logger.info("Recognize completed: data=%s", event.data) + if event.data['recognitionType'] == "choices": + label_detected = event.data['choiceResult']['label']; + phraseDetected = event.data['choiceResult']['recognizedPhrase']; + app.logger.info("Recognition completed, labelDetected=%s, phraseDetected=%s, context=%s", label_detected, phraseDetected, event.data.get('operationContext')) + + call_connection_client.hold(target_participant=PhoneNumberIdentifier(TARGET_PHONE_NUMBER)) + play_source = TextSource(text="You are on hold, Please wait.", voice_name=SPEECH_TO_TEXT_VOICE) + # play_source = FileSource("https://www2.cs.uic.edu/~i101/SoundFiles/StarWars3.wav") + # call_connection_client.hold( + # target_participant=PhoneNumberIdentifier(TARGET_PHONE_NUMBER), + # # play_source=play_source, + # operation_context="holdUserContext", + # operation_callback_url=CALLBACK_EVENTS_URI + # ) + app.logger.info("Participant on hold..") + app.logger.info("Waiting...") + time.sleep(10) + call_connection_client.unhold(target_participant=PhoneNumberIdentifier(TARGET_PHONE_NUMBER)) + # call_connection_client.unhold( + # target_participant=PhoneNumberIdentifier(TARGET_PHONE_NUMBER), + # operation_context="holdUserContext") + app.logger.info("Participant on unhold..") + elif event.data['recognitionType'] == "dtmf": + tones = event.data['dtmfResult']['tones'] + app.logger.info("Recognition completed, tones=%s, context=%s", tones, event.data.get('operationContext')) + # call_connection_client.hang_up(is_for_everyone=True) + elif event.data['recognitionType'] == "speech": + text = event.data['speechResult']['speech']; + app.logger.info("Recognition completed, text=%s, context=%s", text, event.data.get('operationContext')) + # call_connection_client.hang_up(is_for_everyone=True) + # handle_play(call_connection_client=call_connection_client, text_to_play="Recognized successfully",context="textSourceContext") + call_connection_client.hang_up(is_for_everyone=True) + + elif event.type == "Microsoft.Communication.RecognizeFailed": + # failedContext = event.data['operationContext'] + app.logger.info("Recognize Failed: data=%s", event.data) + # if(failedContext and failedContext == RETRY_CONTEXT): + # handle_play(call_connection_client=call_connection_client, text_to_play=NO_RESPONSE) + # else: + # resultInformation = event.data['resultInformation'] + # app.logger.info("Encountered error during recognize, message=%s, code=%s, subCode=%s", + # resultInformation['message'], + # resultInformation['code'], + # resultInformation['subCode']) + # if(resultInformation['subCode'] in[8510, 8510]): + # textToPlay =CUSTOMER_QUERY_TIMEOUT + # else : + # textToPlay =INVALID_AUDIO + + resultInformation = event.data['resultInformation'] + app.logger.info("Encountered error during recognize, message=%s, code=%s, subCode=%s", + resultInformation['message'], + resultInformation['code'], + resultInformation['subCode']) + # get_media_recognize_options( + # call_connection_client=call_connection_client, + # text_to_play=textToPlay, + # target_participant=target_participant, + # choices=get_choices(),context=RETRY_CONTEXT) + # call_connection_client.hang_up(is_for_everyone=True) + handle_play(call_connection_client=call_connection_client, text_to_play="Recognized Failed",context="textSourceContext") + + elif event.type in ["Microsoft.Communication.PlayCompleted"]: + app.logger.info("Play completed: data=%s", event.data) + app.logger.info("Terminating call") + call_connection_client.hang_up(is_for_everyone=True) + elif event.type in ["Microsoft.Communication.PlayFailed"]: + app.logger.info("Play Failed: data=%s", event.data) + resultInformation = event.data['resultInformation'] + app.logger.info("Encountered error during play, message=%s, code=%s, subCode=%s", + resultInformation['message'], + resultInformation['code'], + resultInformation['subCode']) + call_connection_client.hang_up(is_for_everyone=True) + return Response(status=200) + +# GET endpoint to render the menus +@app.route('/') +def index_handler(): + return render_template("index.html") + + +if __name__ == '__main__': + app.logger.setLevel(INFO) + app.run(port=8080) diff --git a/beta4-media-test-outbound/readme.md b/beta4-media-test-outbound/readme.md new file mode 100644 index 0000000..1c9078c --- /dev/null +++ b/beta4-media-test-outbound/readme.md @@ -0,0 +1,67 @@ +|page_type| languages |products +|---|-----------------------------------------|---| +|sample|
Python
|
azureazure-communication-services
| + +# Call Automation - Quick Start Sample + +In this quickstart, we cover how you can use Call Automation SDK to make an outbound call to a phone number and use the newly announced integration with Azure AI services to play dynamic prompts to participants using Text-to-Speech and recognize user voice input through Speech-to-Text to drive business logic in your application. + +# Design + +![design](./data/OutboundCallDesign.png) + +## Prerequisites + +- An Azure account with an active subscription. [Create an account for free](https://azure.microsoft.com/free/?WT.mc_id=A261C142F). +- A deployed Communication Services resource. [Create a Communication Services resource](https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource). +- A [phone number](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/telephony/get-phone-number) in your Azure Communication Services resource that can make outbound calls. NB: phone numbers are not available in free subscriptions. +- Create Azure AI Multi Service resource. For details, see [Create an Azure AI Multi service](https://learn.microsoft.com/en-us/azure/cognitive-services/cognitive-services-apis-create-account). +- Create and host a Azure Dev Tunnel. Instructions [here](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started) +- [Python](https://www.python.org/downloads/) 3.7 or above. +- (Optional) A Microsoft Teams user with a phone license that is `voice` enabled. Teams phone license is required to add Teams users to the call. Learn more about Teams licenses [here](https://www.microsoft.com/microsoft-teams/compare-microsoft-teams-bundle-options). Learn about enabling phone system with `voice` [here](https://learn.microsoft.com/microsoftteams/setting-up-your-phone-system). You also need to complete the prerequisite step [Authorization for your Azure Communication Services Resource](https://learn.microsoft.com/azure/communication-services/how-tos/call-automation/teams-interop-call-automation?pivots=programming-language-javascript#step-1-authorization-for-your-azure-communication-services-resource-to-enable-calling-to-microsoft-teams-users) to enable calling to Microsoft Teams users. + +## Before running the sample for the first time + +1. Open an instance of PowerShell, Windows Terminal, Command Prompt or equivalent and navigate to the directory that you would like to clone the sample to. +2. git clone `https://github.com/Azure-Samples/communication-services-python-quickstarts.git`. +3. Navigate to `callautomation-outboundcalling` folder and open `main.py` file. + +### Setup the Python environment + +Create and activate python virtual environment and install required packages using following command +``` +pip install -r requirements.txt +``` + +### Setup and host your Azure DevTunnel + +[Azure DevTunnels](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview) is an Azure service that enables you to share local web services hosted on the internet. Use the commands below to connect your local development environment to the public internet. This creates a tunnel with a persistent endpoint URL and which allows anonymous access. We will then use this endpoint to notify your application of calling events from the ACS Call Automation service. + +```bash +devtunnel create --allow-anonymous +devtunnel port create -p 8080 +devtunnel host +``` + +### Configuring application + +Open `main.py` file to configure the following settings + +1. `ACS_CONNECTION_STRING`: Azure Communication Service resource's connection string. +2. `ACS_PHONE_NUMBER`: Phone number associated with the Azure Communication Service resource. For e.g. "+1425XXXAAAA" +3. `TARGET_PHONE_NUMBER`: Target phone number to add in the call. For e.g. "+1425XXXAAAA" +4. `CALLBACK_URI_HOST`: Base url of the app. (For local development use dev tunnel url) +5. `COGNITIVE_SERVICES_ENDPOINT`: Cognitive Service Endpoint +6. `TARGET_TEAMS_USER_ID`: (Optional) update field with the Microsoft Teams user Id you would like to add to the call. See [Use Graph API to get Teams user Id](https://learn.microsoft.com/azure/communication-services/how-tos/call-automation/teams-interop-call-automation?pivots=programming-language-python#step-2-use-the-graph-api-to-get-microsoft-entra-object-id-for-teams-users-and-optionally-check-their-presence). Uncomment the below snippet in main.py to enable Teams Interop scenario. + +```python +call_connection_client.add_participant(target_participant = CallInvite( + target = MicrosoftTeamsUserIdentifier(user_id=TARGET_TEAMS_USER_ID), + source_display_name = "Jack (Contoso Tech Support)")) +``` + +## Run app locally + +1. Navigate to `callautomation-outboundcalling` folder and run `main.py` in debug mode or use command `python ./main.py` to run it from PowerShell, Command Prompt or Unix Terminal +2. Browser should pop up with the below page. If not navigate it to `http://localhost:8080/` or your dev tunnel url. +3. To initiate the call, click on the `Place a call!` button or make a Http get request to `https:///outboundCall` diff --git a/beta4-media-test-outbound/requirements.txt b/beta4-media-test-outbound/requirements.txt new file mode 100644 index 0000000..5db9d51 --- /dev/null +++ b/beta4-media-test-outbound/requirements.txt @@ -0,0 +1,3 @@ +Flask>=2.3.2 +azure-eventgrid==4.11.0 +azure-communication-callautomation==1.1.0 diff --git a/beta4-media-test-outbound/template/index.html b/beta4-media-test-outbound/template/index.html new file mode 100644 index 0000000..7609422 --- /dev/null +++ b/beta4-media-test-outbound/template/index.html @@ -0,0 +1,15 @@ + + + + Azure Communication Services Quickstart + + +

Azure Communication Services

+

Outbound Calling Quickstart

+
+
+ +
+
+ + \ No newline at end of file