diff --git a/Python-MoveParticipantsSample/Resources/Move_Participant_Sample.jpg b/Python-MoveParticipantsSample/Resources/Move_Participant_Sample.jpg
new file mode 100644
index 0000000..9bd4c4d
Binary files /dev/null and b/Python-MoveParticipantsSample/Resources/Move_Participant_Sample.jpg differ
diff --git a/Python-MoveParticipantsSample/main.py b/Python-MoveParticipantsSample/main.py
new file mode 100644
index 0000000..af24555
--- /dev/null
+++ b/Python-MoveParticipantsSample/main.py
@@ -0,0 +1,452 @@
+import logging
+import json
+from typing import List, Optional, Dict, Any
+from urllib.parse import urljoin
+from fastapi import FastAPI, HTTPException, status, Body
+from fastapi.responses import PlainTextResponse, Response
+from pydantic import BaseModel
+from azure.eventgrid import EventGridEvent, SystemEventNames
+from azure.core.messaging import CloudEvent
+from azure.communication.callautomation.aio import CallAutomationClient
+from azure.communication.callautomation import (
+ PhoneNumberIdentifier,
+ CommunicationUserIdentifier
+)
+
+# Configuration constants
+ACS_CONNECTION_STRING=""
+CALLBACK_URI_HOST=""
+ACS_OUTBOUND_PHONE_NUMBER=""
+ACS_INBOUND_PHONE_NUMBER=""
+ACS_USER_PHONE_NUMBER=""
+ACS_TEST_IDENTITY2=""
+ACS_TEST_IDENTITY3=""
+
+# Configure logging
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
+
+# FastAPI application setup
+app = FastAPI(
+ title="Move Participants Sample",
+ description="Azure Communication Services Call Automation Move Participants Sample",
+ version="1.0.0"
+)
+
+# Global variables
+acs_connection_string = ACS_CONNECTION_STRING
+callback_uri_host = CALLBACK_URI_HOST
+acs_outbound_phone_number = ACS_OUTBOUND_PHONE_NUMBER
+acs_inbound_phone_number = ACS_INBOUND_PHONE_NUMBER
+acs_user_phone_number = ACS_USER_PHONE_NUMBER
+acs_test_identity2 = ACS_TEST_IDENTITY2
+acs_test_identity3 = ACS_TEST_IDENTITY3
+last_workflow_call_type = ""
+call_connection_id = ""
+call_connection_id1 = ""
+call_connection_id2 = ""
+client: Optional[CallAutomationClient] = None
+
+# Pydantic models
+class ConfigurationRequest(BaseModel):
+ acs_connection_string: Optional[str] = None
+ callback_uri_host: Optional[str] = None
+ acs_outbound_phone_number: Optional[str] = None
+ acs_inbound_phone_number: Optional[str] = None
+ acs_user_phone_number: Optional[str] = None
+ acs_test_identity2: Optional[str] = None
+ acs_test_identity3: Optional[str] = None
+
+class MoveParticipantsRequest(BaseModel):
+ participant_to_move: str
+ source_call_connection_id: str
+ target_call_connection_id: str
+
+# Utility functions
+def initialize_client():
+ """Initialize the CallAutomationClient with current configuration"""
+ global client
+ if acs_connection_string:
+ client = CallAutomationClient.from_connection_string(acs_connection_string)
+ logger.info("Call automation client initialized successfully")
+ else:
+ logger.warning("Cannot initialize client: ACS connection string not provided")
+
+def get_callback_uri() -> str:
+ """Get the callback URI for the application"""
+ return urljoin(callback_uri_host, "/api/callbacks")
+
+def create_participant_identifier(participant_id: str):
+ """Create appropriate participant identifier based on input format"""
+ if participant_id.startswith("+"):
+ return PhoneNumberIdentifier(participant_id)
+ elif participant_id.startswith("8:acs:"):
+ return CommunicationUserIdentifier(participant_id)
+ else:
+ raise ValueError("Invalid participant format. Use phone number (+1234567890) or ACS user ID (8:acs:...)")
+
+def extract_caller_ids(event_data: Dict[str, Any]) -> tuple[str, str]:
+ """Extract from and to caller IDs from event data"""
+ from_caller_id = (event_data['from']["phoneNumber"]["value"]
+ if event_data['from']['kind'] == "phoneNumber"
+ else event_data['from']['rawId'])
+ to_caller_id = (event_data['to']["phoneNumber"]["value"]
+ if event_data['to']['kind'] == "phoneNumber"
+ else event_data['to']['rawId'])
+ return from_caller_id, to_caller_id
+
+async def handle_user_incoming_call(incoming_call_data: Dict[str, Any], from_caller_id: str, to_caller_id: str) -> List[str]:
+ """Handle incoming call from user"""
+ global call_connection_id1
+ callback_uri = get_callback_uri()
+
+ answer_call_result = await client.answer_call(
+ incoming_call_context=incoming_call_data.get("incomingCallContext"),
+ callback_url=callback_uri,
+ operation_context="IncomingCallFromUser"
+ )
+
+ call_connection_id1 = answer_call_result.call_connection_id
+ logger.info(f"User call answered - Connection ID: {call_connection_id1}")
+
+ return [
+ "User call answered by Call Automation",
+ f"From: {from_caller_id}",
+ f"To: {to_caller_id}",
+ f"Connection ID: {call_connection_id1}",
+ f"Correlation ID: {incoming_call_data.get('correlationId', 'N/A')}"
+ ]
+
+async def handle_workflow_call_redirect(incoming_call_data: Dict[str, Any], from_caller_id: str, to_caller_id: str) -> List[str]:
+ """Handle workflow call redirection to ACS identities"""
+ incoming_call_context = incoming_call_data.get("incomingCallContext")
+
+ if last_workflow_call_type == "CallTwo":
+ await client.redirect_call(
+ incoming_call_context=incoming_call_context,
+ target_participant=CommunicationUserIdentifier(acs_test_identity2)
+ )
+ logger.info(f"Call2 redirected to ACS User Identity 2: {acs_test_identity2}")
+ return [
+ f"Call2 redirected to ACS User Identity 2: {acs_test_identity2}",
+ f"From: {from_caller_id}",
+ f"To: {to_caller_id}"
+ ]
+
+ elif last_workflow_call_type == "CallThree":
+ await client.redirect_call(
+ incoming_call_context=incoming_call_context,
+ target_participant=CommunicationUserIdentifier(acs_test_identity3)
+ )
+ logger.info(f"Call3 redirected to ACS User Identity 3: {acs_test_identity3}")
+ return [
+ f"Call3 redirected to ACS User Identity 3: {acs_test_identity3}",
+ f"From: {from_caller_id}",
+ f"To: {to_caller_id}"
+ ]
+
+ else:
+ logger.warning(f"Unknown workflow call type: {last_workflow_call_type}. Using default behavior.")
+ await client.redirect_call(
+ incoming_call_context=incoming_call_context,
+ target_participant=CommunicationUserIdentifier(acs_test_identity2)
+ )
+ return [f"Default: Redirected to ACS User Identity 2: {acs_test_identity2}"]
+
+def format_response_message(messages: List[str]) -> str:
+ """Format response messages for consistent output"""
+ return "\n".join(messages)
+
+def validate_client():
+ """Validate that the client is initialized"""
+ if not client:
+ raise HTTPException(status_code=500, detail="CallAutomationClient not initialized")
+
+# Initialize client on startup
+initialize_client()
+
+# API Endpoints
+@app.post("/api/MoveParticipantEvent")
+async def move_participant_event(events: List[Dict[str, Any]] = Body(...)):
+ """Handle Event Grid events for move participants scenario"""
+ validate_client()
+
+ logger.info("Processing Event Grid events")
+
+ try:
+ for event_data in events:
+ event = EventGridEvent.from_dict(event_data)
+ logger.info(f"Processing event: {event.event_type}")
+
+ if event.event_type == SystemEventNames.EventGridSubscriptionValidationEventName:
+ validation_code = event.data.get("validationCode", "")
+ logger.info("Event Grid subscription validation completed")
+ return Response(
+ content=json.dumps({"validationResponse": validation_code}),
+ status_code=200,
+ media_type="application/json"
+ )
+
+ elif event.event_type == "Microsoft.Communication.IncomingCall":
+ from_caller_id, to_caller_id = extract_caller_ids(event.data)
+ logger.info(f"Incoming call from {from_caller_id} to {to_caller_id}")
+
+ msg_log = []
+
+ if acs_user_phone_number in from_caller_id:
+ msg_log = await handle_user_incoming_call(event.data, from_caller_id, to_caller_id)
+
+ elif acs_inbound_phone_number in from_caller_id:
+ msg_log = await handle_workflow_call_redirect(event.data, from_caller_id, to_caller_id)
+
+ response_content = format_response_message(msg_log) if msg_log else "Event processed"
+ return Response(content=response_content, status_code=200)
+
+ return Response(content="Events processed successfully", status_code=200)
+
+ except Exception as e:
+ logger.error(f"Error processing Event Grid event: {e}")
+ return Response(content=f"Error: {str(e)}", status_code=500)
+
+@app.post("/api/callbacks")
+async def callbacks(events: List[Dict[str, Any]] = Body(...)):
+ """Handle Call Automation callback events"""
+ validate_client()
+
+ msg_log = []
+
+ try:
+ for event_data in events:
+ cloud_event = CloudEvent.from_dict(event_data)
+ call_connection_id = cloud_event.data.get('callConnectionId')
+ operation_context = cloud_event.data.get('operationContext', '')
+ event_type = cloud_event.data.get('type', cloud_event.type)
+
+ logger.info(f"Callback event: {event_type}, Context: {operation_context}")
+
+ if operation_context and "CallConnected" in event_type:
+ if operation_context in ["CallTwo", "CallThree"]:
+ msg_log.extend([
+ f"Call event: CallConnected",
+ f"{operation_context} Connection ID: {call_connection_id}",
+ f"Correlation ID: {cloud_event.data.get('correlationId', 'N/A')}"
+ ])
+
+ elif "CallDisconnected" in event_type:
+ msg_log.extend([
+ f"Call event: CallDisconnected",
+ f"Connection ID: {call_connection_id}"
+ ])
+
+ response_content = format_response_message(msg_log) if msg_log else ""
+ return PlainTextResponse(content=response_content)
+
+ except Exception as e:
+ logger.error(f"Error processing callback: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/CreateCall1(UserCallToCallAutomation)", tags=["Move Participants APIs"])
+async def create_call1():
+ """Create Call 1 - User Call to Call Automation"""
+ global call_connection_id
+ validate_client()
+
+ try:
+ callback_uri = get_callback_uri()
+ caller = PhoneNumberIdentifier(acs_user_phone_number)
+
+ create_call_result = await client.create_call(
+ target_participant=PhoneNumberIdentifier(acs_inbound_phone_number),
+ callback_url=callback_uri,
+ source_caller_id_number=caller
+ )
+
+ call_connection_id = create_call_result.call_connection_id
+ logger.info(f"Call1 created - Connection ID: {call_connection_id}")
+
+ messages = [
+ "Call 1 (External PSTN to Call Automation):",
+ f"From: {acs_user_phone_number}",
+ f"To: {acs_inbound_phone_number}",
+ f"Target Connection ID: {call_connection_id}",
+ f"Correlation ID: {create_call_result.correlation_id}"
+ ]
+
+ return PlainTextResponse(content=format_response_message(messages))
+
+ except Exception as e:
+ logger.error(f"Error creating Call 1: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/CreateCall2(ToPstnUserFirstAndRedirectToAcsIentity)", tags=["Move Participants APIs"])
+async def create_call2():
+ """Create Call 2 - To PSTN User First And Redirect To ACS Identity"""
+ global last_workflow_call_type, call_connection_id1
+ validate_client()
+
+ try:
+ callback_uri = get_callback_uri()
+ caller = PhoneNumberIdentifier(acs_inbound_phone_number)
+
+ create_call_result = await client.create_call(
+ target_participant=PhoneNumberIdentifier(acs_outbound_phone_number),
+ callback_url=callback_uri,
+ source_caller_id_number=caller,
+ operation_context="CallTwo"
+ )
+
+ last_workflow_call_type = "CallTwo"
+ call_connection_id1 = create_call_result.call_connection_id
+ logger.info(f"Call2 created - Connection ID: {call_connection_id1}")
+
+ messages = [
+ "Call 2:",
+ f"From: {acs_inbound_phone_number}",
+ f"To: {acs_outbound_phone_number}",
+ f"Source Connection ID: {call_connection_id1}",
+ f"Correlation ID: {create_call_result.correlation_id}",
+ f"Will redirect to: {acs_test_identity2}"
+ ]
+
+ return PlainTextResponse(content=format_response_message(messages))
+
+ except Exception as e:
+ logger.error(f"Error creating Call 2: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/CreateCall3(ToPstnUserFirstAndRedirectToAcsIentity)", tags=["Move Participants APIs"])
+async def create_call3():
+ """Create Call 3 - To PSTN User First And Redirect To ACS Identity"""
+ global last_workflow_call_type, call_connection_id2
+ validate_client()
+
+ try:
+ callback_uri = get_callback_uri()
+ caller = PhoneNumberIdentifier(acs_inbound_phone_number)
+
+ create_call_result = await client.create_call(
+ target_participant=PhoneNumberIdentifier(acs_outbound_phone_number),
+ callback_url=callback_uri,
+ source_caller_id_number=caller,
+ operation_context="CallThree"
+ )
+
+ last_workflow_call_type = "CallThree"
+ call_connection_id2 = create_call_result.call_connection_id
+ logger.info(f"Call3 created - Connection ID: {call_connection_id2}")
+
+ messages = [
+ "Call 3:",
+ f"From: {acs_inbound_phone_number}",
+ f"To: {acs_outbound_phone_number}",
+ f"Source Connection ID: {call_connection_id2}",
+ f"Correlation ID: {create_call_result.correlation_id}",
+ f"Will redirect to: {acs_test_identity3}"
+ ]
+
+ return PlainTextResponse(content=format_response_message(messages))
+
+ except Exception as e:
+ logger.error(f"Error creating Call 3: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/MoveParticipant", tags=["Move Participants APIs"])
+async def move_participant(request: MoveParticipantsRequest):
+ """Move participants between calls"""
+ validate_client()
+
+ try:
+ logger.info(f"Moving participant {request.participant_to_move} from {request.source_call_connection_id} to {request.target_call_connection_id}")
+
+ target_connection = client.get_call_connection(request.target_call_connection_id)
+ participant_to_move = create_participant_identifier(request.participant_to_move)
+
+ response = await target_connection.move_participants(
+ target_participants=[participant_to_move],
+ from_call=request.source_call_connection_id
+ )
+
+ if response:
+ logger.info("Move participants operation completed successfully")
+ messages = [
+ "Move Participant Operation:",
+ f"Participant: {request.participant_to_move}",
+ f"From: {request.source_call_connection_id}",
+ f"To: {request.target_call_connection_id}",
+ "Status: Success"
+ ]
+ else:
+ raise Exception("Move participants operation failed")
+
+ return PlainTextResponse(content=format_response_message(messages))
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error in move participants operation: {e}")
+ raise HTTPException(
+ status_code=400,
+ detail={
+ "success": False,
+ "error": str(e),
+ "message": "Move participants operation failed"
+ }
+ )
+
+@app.get("/GetParticipants/{call_connection_id}", tags=["Move Participants APIs"])
+async def get_participants(call_connection_id: str):
+ """Get participants for a specific call connection"""
+ validate_client()
+
+ try:
+ call_connection = client.get_call_connection(call_connection_id)
+ participants_pager = call_connection.list_participants()
+
+ participant_info = []
+ participant_count = 0
+
+ async for participant in participants_pager:
+ participant_count += 1
+ if hasattr(participant, 'identifier'):
+ identifier = participant.identifier
+ if isinstance(identifier, PhoneNumberIdentifier):
+ info = f"{participant_count}. PhoneNumberIdentifier - RawId: {identifier.raw_id}"
+ elif isinstance(identifier, CommunicationUserIdentifier):
+ info = f"{participant_count}. CommunicationUserIdentifier - RawId: {identifier.raw_id}"
+ else:
+ info = f"{participant_count}. {type(identifier).__name__} - RawId: {identifier.raw_id}"
+ participant_info.append(info)
+
+ if participant_count == 0:
+ raise HTTPException(
+ status_code=404,
+ detail={
+ "message": "No participants found for the specified call connection",
+ "call_connection_id": call_connection_id
+ }
+ )
+
+ logger.info(f"Retrieved {participant_count} participants for call {call_connection_id}")
+
+ messages = [
+ f"Participants for Call {call_connection_id}:",
+ f"Count: {participant_count}",
+ ""] + participant_info
+
+ return PlainTextResponse(content=format_response_message(messages))
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error getting participants for call {call_connection_id}: {e}")
+ raise HTTPException(
+ status_code=400,
+ detail={
+ "error": str(e),
+ "call_connection_id": call_connection_id
+ }
+ )
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8080)
\ No newline at end of file
diff --git a/Python-MoveParticipantsSample/readme.md b/Python-MoveParticipantsSample/readme.md
new file mode 100644
index 0000000..998e1dd
--- /dev/null
+++ b/Python-MoveParticipantsSample/readme.md
@@ -0,0 +1,252 @@
+| page_type | languages | products |
+| --------- | --------------------------------------- | --------------------------------------------------------------------------- |
+| sample |
| | azure | azure-communication-services |
|
+
+# Call Automation - Move Participants Sample
+
+This sample demonstrates how to use the Call Automation SDK to implement a Move Participants Call scenario with Azure Communication Services.
+
+---
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Design](#design)
+- [Prerequisites](#prerequisites)
+- [Getting Started](#getting-started)
+- [Configuration](#configuration)
+- [Running the App Locally](#running-the-app-locally)
+- [Troubleshooting](#troubleshooting)
+
+---
+
+## Overview
+
+This project provides a sample implementation for moving participants between calls using Azure Communication Services and the Call Automation SDK.
+
+---
+
+## Design
+
+
+
+---
+
+## Prerequisites
+
+- **Azure Account:** An Azure account with an active subscription.
+ https://azure.microsoft.com/free/?WT.mc_id=A261C142F.
+- **Communication Services Resource:** A deployed Communication Services resource.
+ https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource.
+- **Phone Number:** A 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.
+- **Azure Dev Tunnel:** https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started.
+
+- [Python](https://www.python.org/downloads/) 3.7 or above.
+
+---
+
+## Getting Started
+
+### Clone the Source Code
+
+1. Open PowerShell, Windows Terminal, Command Prompt, or equivalent.
+2. Navigate to your desired directory.
+3. Clone the repository:
+ ```sh
+ git clone https://github.com/Azure-Samples/communication-services-python-quickstarts.git
+ ```
+
+### Set Up Python Virtual Environment and install Dependencies
+
+```bash
+cd communication-services-python-quickstarts/Python-MoveParticipantsSample
+python -m venv venv
+venv\Scripts\activate
+pip install -r requirements.txt
+```
+
+## Setup and Host Azure Dev Tunnel
+
+```
+devtunnel create --allow-anonymous
+devtunnel port create -p 8080
+devtunnel host
+```
+
+---
+
+## Configuration
+
+Before running the application, initialize the following constants in the `main.py` file.
+
+| Setting | Description | Example Value |
+| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
+| `ACS_CONNECTION_STRING` | The connection string for your Azure Communication Services resource. Find this in the Azure Portal under your resource Keys section. | `"endpoint=https://.communication.azure.com/;accesskey="` |
+| `CALLBACK_URI_HOST` | The base URL where your app will listen for incoming events from Azure Communication Services. For local development, use your Azure Dev Tunnel URL. | `"https://.devtunnels.ms"` |
+| `ACS_OUTBOUND_PHONE_NUMBER` | The Azure Communication Services phone number used to make outbound calls. Must be purchased and configured in your ACS resource. | `"+1XXXXXXXXXX"` |
+| `ACS_INBOUND_PHONE_NUMBER` | The Azure Communication Services phone number used to receive inbound calls. Must also be configured in your ACS resource. | `"+1XXXXXXXXXX"` |
+| `ACS_USER_PHONE_NUMBER` | The phone number of the external user to initiate the first call. Any valid phone number for testing. | `"+1XXXXXXXXXX"` |
+| `ACS_TEST_IDENTITY2` | An Azure Communication Services user identity, generated using the ACS web client or SDK, used for testing participant movement. | `"8:acs:"` |
+| `ACS_TEST_IDENTITY3` | Another ACS user identity, generated similarly, for additional test scenarios. | `"8:acs:"` |
+
+### How to Obtain These Values
+
+- **ACS_CONNECTION_STRING:**
+
+ 1. Go to the Azure Portal.
+ 2. Navigate to your Communication Services resource.
+ 3. Select "Keys & Connection String."
+ 4. Copy the "Connection String" value.
+
+- **CALLBACK_URI_HOST:**
+
+ 1. Set up an Azure Dev Tunnel as described in the prerequisites.
+ 2. Use the public URL provided by the Dev Tunnel as your callback URI host.
+
+- **ACS_OUTBOUND_PHONE_NUMBER / ACS_INBOUND_PHONE_NUMBER:**
+
+ 1. In your Communication Services resource, go to "Phone numbers."
+ 2. Purchase or use an existing phone number.
+ 3. Assign the number as needed for outbound/inbound use.
+
+- **ACS_USER_PHONE_NUMBER:**
+ Use any valid phone number you have access to for testing outbound calls.
+
+- **ACS_TEST_IDENTITY2 / ACS_TEST_IDENTITY3:**
+ 1. Use the ACS web client or SDK to generate user identities.
+ 2. Store the generated identity strings here.
+
+#### Example `config variables`
+
+```
+ ACS_CONNECTION_STRING = "endpoint=https://.communication.azure.com/;accesskey=",
+ CALLBACK_URI_HOST = "https://.devtunnels.ms",
+ ACS_OUTBOUND_PHONE_NUMBER = "+1XXXXXXXXXX",
+ ACS_INBOUND_PHONE_NUMBER = "+1XXXXXXXXXX",
+ ACS_USER_PHONE_NUMBER = "+1XXXXXXXXXX",
+ ACS_TEST_IDENTITY2 = "8:acs:",
+ ACS_TEST_IDENTITY3 = "8:acs:"
+
+```
+
+---
+## Running the App Locally
+
+1. **Create an azure event grid subscription for incoming calls:**
+ - Set up a Web hook(`https:///api/MoveParticipantEvent`) for callback.
+ - Add Filters:
+ - Key: `data.From.PhoneNumber.Value`, operator: `string contains`, value: `acsUserPhoneNumber, Inbound Number (ACS)`
+ - Key: `data.to.rawid`, operator: `string does not begin`, value: `8`
+ - Deploy the event subscription.
+
+2. **Run the Application:**
+ - Navigate to the `MoveParticipantsSample` folder.
+ - Run the application in debug mode.
+
+3. **Workflow Execution**
+
+> **Note:**
+> The phone numbers used here are taken from the Azure Communication Services resource.
+> The phone numbers are released and become available when the call is answered.
+>
+> **Call 2 and Call 3 must be answered after redirecting and before moving participants.**
+
+
+##### Call 1
+
+1. `USER_PHONE_NUMBER` calls `ACS_INBOUND_PHONE_NUMBER`.
+2. When the call is created, note the Call Connection Id as **Target Call Connection Id**.
+3. Call Automation answers the call and assigns a bot as the receiver.
+4. `ACS_INBOUND_PHONE_NUMBER` is released from the call after it is answered and assigned to the bot.
+
+
+##### Call 2
+
+1. `ACS_INBOUND_PHONE_NUMBER` makes a call to `ACS_OUTBOUND_PHONE_NUMBER`.
+2. When the call is created, note the Call Connection Id as **Source Call Connection Id**.
+3. Call Automation answers the call, redirects to `ACS_TEST_IDENTITY_2`, and releases `ACS_OUTBOUND_PHONE_NUMBER` from the call.
+4. The call connection id generated while redirection is an internal connection id; **do not use this connection id for the Move operation**.
+
+##### Move Participant Operation
+
+- **Inputs:**
+ - Source Connection Id (from Call 2): the connection to move the participant from.
+ - Target Connection Id (from Call 1): the connection to move the participant to.
+ - Participant (initial participant before call is redirected) from Source call (Call 2): `ACS_OUTBOUND_PHONE_NUMBER`
+- Participants list after `MoveParticipantSucceeded` event: 3
+
+
+##### Call 3
+
+1. `ACS_INBOUND_PHONE_NUMBER` makes a call to `ACS_OUTBOUND_PHONE_NUMBER`.
+2. When the call is created, note the Call Connection Id as **Source Call Connection Id**.
+3. Call Automation answers the call, redirects to `ACS_TEST_IDENTITY_3`, and releases `ACS_OUTBOUND_PHONE_NUMBER` from the call.
+4. The call connection id generated while redirection is an internal connection id; **do not use this connection id for the Move operation**.
+
+##### Move Participant Operation
+
+- Inputs:
+ - Source Connection Id (from Call 3): the connection to move the participant from.
+ - Target Connection Id (from Call 1): the connection to move the participant to.
+ - Participant (initial participant before call is redirected) from Source call (Call 3): `ACS_OUTBOUND_PHONE_NUMBER`
+- Participants list after `MoveParticipantSucceeded` event: 4
+
+---
+
+## API Testing with Swagger
+
+You can explore and test the available API endpoints using the built-in Swagger UI:
+
+- **Swagger URL:**
+ [https://localhost:8080/docs](https://localhost:8080/docs)
+
+> If running in a dev tunnel or cloud environment, replace `localhost:8080` with your tunnel's public URL (e.g., `https://.devtunnels.ms/docs`).
+
+---
+
+## Troubleshooting
+
+If you encounter issues while setting up or running the Call Automation sample, refer to the following troubleshooting tips:
+
+### 1. Azure Communication Services Connection Issues
+
+- **Error:** "Invalid connection string"
+ **Solution:** Double-check your `ACS_CONNECTION_STRING`. Ensure there are no extra spaces or missing characters. Obtain the connection string directly from the Azure Portal.
+
+- **Error:** "Resource not found"
+ **Solution:** Verify that your Azure Communication Services resource exists and is in the correct subscription and region.
+
+### 2. Dev Tunnel or Callback Issues
+
+- **Error:** "Callback URL not reachable" or events not triggering
+ **Solution:**
+ - Ensure your Azure Dev Tunnel is running and the URL in `CALLBACK_URI_HOST` matches the tunnel's public URL.
+ - Confirm your firewall or network settings allow inbound connections to your local machine.
+ - Make sure the application is running and listening on the correct port.
+
+### 3. Phone Number Problems
+
+- **Error:** "Phone number not provisioned" or "Invalid phone number"
+ **Solution:**
+ - Confirm that the phone numbers in `ACS_OUTBOUND_PHONE_NUMBER` and `ACS_INBOUND_PHONE_NUMBER` are purchased and assigned in your Azure Communication Services resource.
+ - Use format (e.g., `+1XXXXXXXXXX`).
+
+### 4. Identity or Participant Issues
+
+- **Error:** "Invalid ACS identity"
+ **Solution:**
+ - Ensure `ACS_TEST_IDENTITY2` and `ACS_TEST_IDENTITY3` are valid ACS user identities generated via the ACS SDK or portal.
+ - Regenerate identities if needed and update `main.py`.
+
+### 5. General Debugging Tips
+
+- Check application logs for detailed error messages.
+- Ensure all configuration settings are correct.
+- Restart your application and Dev Tunnel after making configuration changes.
+- Review Azure Portal for resource status and quotas.
+
+**Still having trouble?**
+
+- Review the official https://learn.microsoft.com/azure/communication-services/.
+- Search for similar issues or ask questions on https://learn.microsoft.com/answers/topics/azure-communication-services.html.
+- Contact your Azure administrator or support team if you suspect a permissions or resource issue.
diff --git a/Python-MoveParticipantsSample/requirements.txt b/Python-MoveParticipantsSample/requirements.txt
new file mode 100644
index 0000000..44abb3a
--- /dev/null
+++ b/Python-MoveParticipantsSample/requirements.txt
@@ -0,0 +1,7 @@
+fastapi>=0.110.0
+uvicorn[standard]>=0.27.0
+azure-eventgrid==4.11.0
+azure-communication-callautomation==1.6.0-beta.1
+aiohttp>=3.11.18
+jinja2>=3.1.2
+pydantic>=2.0
diff --git a/README.md b/README.md
index a63ade4..8f4d3be 100644
--- a/README.md
+++ b/README.md
@@ -23,3 +23,7 @@ Azure Communication Services enable developers to add communication capabilities
6. [Use managed identities](https://docs.microsoft.com/azure/communication-services/quickstarts/managed-identity?pivots=programming-language-python)
7. [Get started with Rooms](https://docs.microsoft.com/en-us/azure/communication-services/quickstarts/rooms/get-started-rooms?pivots=programming-language-python)
+
+## Data CollectionAdd commentMore actions
+
+The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. Some Quickstart samples collect information about users and their use of the software that cannot be opted out of. Do not use Quickstart samples that collect information if you wish to avoid telemetry. You can learn more about data collection and use in the help documentation and Microsoft’s [privacy statement](https://go.microsoft.com/fwlink/?LinkID=824704). For more information on the data collected by the Azure SDK, please visit the [Telemetry Policy](https://learn.microsoft.com/azure/communication-services/concepts/privacy) page.
diff --git a/callautomation-azure-openai-voice/azureOpenAIService.py b/callautomation-azure-openai-voice/azureOpenAIService.py
new file mode 100644
index 0000000..c2474ab
--- /dev/null
+++ b/callautomation-azure-openai-voice/azureOpenAIService.py
@@ -0,0 +1,174 @@
+import json
+
+from openai import AsyncAzureOpenAI
+
+import asyncio
+import json
+import random
+
+AZURE_OPENAI_API_ENDPOINT = ''
+AZURE_OPENAI_API_VERSION = "2024-10-01-preview"
+AZURE_OPENAI_API_KEY = ''
+AZURE_OPENAI_DEPLOYMENT_NAME = ''
+SAMPLE_RATE = 24000
+
+def session_config():
+ """Returns a random value from the predefined list."""
+ values = ['alloy', 'ash', 'ballad', 'coral', 'echo', 'sage', 'shimmer', 'verse']
+ ### for details on available param: https://platform.openai.com/docs/api-reference/realtime-sessions/create
+ SESSION_CONFIG={
+ "input_audio_transcription": {
+ "model": "whisper-1",
+ },
+ "turn_detection": {
+ "threshold": 0.4,
+ "silence_duration_ms": 600,
+ "type": "server_vad"
+ },
+ "instructions": "Your name is Sam, you work for Contoso Services. You're a helpful, calm and cheerful agent who responds with a clam British accent, but also can speak in any language or accent. Always start the conversation with a cheery hello, stating your name and who do you work for!",
+ "voice": random.choice(values),
+ "modalities": ["text", "audio"] ## required to solicit the initial welcome message
+ }
+ return SESSION_CONFIG
+
+class OpenAIRTHandler():
+ incoming_websocket = None
+ client = None
+ connection = None
+ connection_manager = None
+ welcomed = False
+
+ def __init__(self) -> None:
+ print("Hello World")
+ self.client = AsyncAzureOpenAI(
+ azure_endpoint=AZURE_OPENAI_API_ENDPOINT,
+ azure_deployment=AZURE_OPENAI_DEPLOYMENT_NAME,
+ api_key=AZURE_OPENAI_API_KEY,
+ api_version=AZURE_OPENAI_API_VERSION,
+ )
+ self.connection_manager = self.client.beta.realtime.connect(
+ model="gpt-4o-realtime-preview" # Replace with your deployed realtime model id on Azure OpenAI.
+ )
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.connection.close()
+ self.incoming_websocket.close()
+
+#start_conversation > start_client
+ async def start_client(self):
+ self.connection = await self.connection_manager.enter()
+ await self.connection.session.update(session=session_config())
+ await self.connection.response.create()
+ ### running an async task to listen and recieve oai messages
+ asyncio.create_task(self.receive_oai_messages())
+
+#send_audio_to_external_ai > audio_to_oai
+ async def audio_to_oai(self, audioData: str):
+ await self.connection.input_audio_buffer.append(audio=audioData)
+
+#receive_messages > receive_oai_messages
+ async def receive_oai_messages(self):
+ #while not self.connection._connection.close_code:
+ async for event in self.connection:
+ #print(event)
+ if event is None:
+ continue
+ match event.type:
+ case "session.created":
+ print("Session Created Message")
+ print(f" Session Id: {event.session.id}")
+ pass
+ case "error":
+ print(f" Error: {event.error}")
+ pass
+ case "input_audio_buffer.cleared":
+ print("Input Audio Buffer Cleared Message")
+ pass
+ case "input_audio_buffer.speech_started":
+ print(f"Voice activity detection started at {event.audio_start_ms} [ms]")
+ await self.stop_audio()
+ pass
+ case "input_audio_buffer.speech_stopped":
+ pass
+ case "conversation.item.input_audio_transcription.completed":
+ print(f" User:-- {event.transcript}")
+ case "conversation.item.input_audio_transcription.failed":
+ print(f" Error: {event.error}")
+ case "response.done":
+ print("Response Done Message")
+ print(f" Response Id: {event.response.id}")
+ if event.response.status_details:
+ print(f" Status Details: {event.response.status_details.model_dump_json()}")
+ case "response.audio_transcript.done":
+ print(f" AI:-- {event.transcript}")
+ case "response.audio.delta":
+ await self.oai_to_acs(event.delta)
+ pass
+ case _:
+ pass
+
+#init_websocket -> init_incoming_websocket (incoming)
+ async def init_incoming_websocket(self, socket):
+ # print("--inbound socket set")
+ self.incoming_websocket = socket
+
+#receive_audio_for_outbound > oai_to_acs
+ async def oai_to_acs(self, data):
+ try:
+ data = {
+ "Kind": "AudioData",
+ "AudioData": {
+ "Data": data
+ },
+ "StopAudio": None
+ }
+
+ # Serialize the server streaming data
+ serialized_data = json.dumps(data)
+ await self.send_message(serialized_data)
+
+ except Exception as e:
+ print(e)
+
+# stop oai talking when detecting the user talking
+ async def stop_audio(self):
+ stop_audio_data = {
+ "Kind": "StopAudio",
+ "AudioData": None,
+ "StopAudio": {}
+ }
+
+ json_data = json.dumps(stop_audio_data)
+ await self.send_message(json_data)
+
+# send_message > send_message
+ async def send_message(self, message: str):
+ try:
+ await self.incoming_websocket.send(message)
+ except Exception as e:
+ print(f"Failed to send message: {e}")
+
+ async def send_welcome(self):
+ if not self.welcomed:
+ await self.connection.conversation.item.create(
+ item={
+ "type": "message",
+ "role": "user",
+ "content": [{"type": "input_text", "text": "Hi! What's your name and who do you work for?"}],
+ }
+ )
+ await self.connection.response.create()
+ self.welcomed = True
+
+#mediaStreamingHandler.process_websocket_message_async -> acs_to_oai
+ async def acs_to_oai(self, stream_data):
+ try:
+ data = json.loads(stream_data)
+ kind = data['kind']
+ if kind == "AudioData":
+ audio_data_section = data.get("audioData", {})
+ if not audio_data_section.get("silent", True):
+ audio_data = audio_data_section.get("data")
+ await self.audio_to_oai(audio_data)
+ except Exception as e:
+ print(f'Error processing WebSocket message: {e}')
diff --git a/callautomation-azure-openai-voice/main.py b/callautomation-azure-openai-voice/main.py
new file mode 100644
index 0000000..38e890b
--- /dev/null
+++ b/callautomation-azure-openai-voice/main.py
@@ -0,0 +1,130 @@
+from quart import Quart, Response, request, json, websocket
+from azure.eventgrid import EventGridEvent, SystemEventNames
+from urllib.parse import urlencode, urlparse, urlunparse
+from logging import INFO
+from azure.communication.callautomation import (
+ MediaStreamingOptions,
+ AudioFormat,
+ MediaStreamingContentType,
+ MediaStreamingAudioChannelType,
+ StreamingTransportType
+ )
+from azure.communication.callautomation.aio import (
+ CallAutomationClient
+ )
+import uuid
+
+from azureOpenAIService import OpenAIRTHandler
+
+# Your ACS resource connection string
+ACS_CONNECTION_STRING = "ACS_CONNECTION_STRING"
+
+# Callback events URI to handle callback events.
+CALLBACK_URI_HOST = "CALLBACK_URI_HOST"
+CALLBACK_EVENTS_URI = CALLBACK_URI_HOST + "/api/callbacks"
+
+acs_client = CallAutomationClient.from_connection_string(ACS_CONNECTION_STRING)
+app = Quart(__name__)
+
+@app.route("/api/incomingCall", methods=['POST'])
+async def incoming_call_handler():
+ app.logger.info("incoming event data")
+ for event_dict in await request.json:
+ event = EventGridEvent.from_dict(event_dict)
+ app.logger.info("incoming event data --> %s", event.data)
+ if event.event_type == SystemEventNames.EventGridSubscriptionValidationEventName:
+ app.logger.info("Validating subscription")
+ validation_code = event.data['validationCode']
+ validation_response = {'validationResponse': validation_code}
+ return Response(response=json.dumps(validation_response), status=200)
+ elif event.event_type =="Microsoft.Communication.IncomingCall":
+ app.logger.info("Incoming call received: data=%s",
+ event.data)
+ if event.data['from']['kind'] =="phoneNumber":
+ caller_id = event.data['from']["phoneNumber"]["value"]
+ else :
+ caller_id = event.data['from']['rawId']
+ app.logger.info("incoming call handler caller id: %s",
+ caller_id)
+ incoming_call_context=event.data['incomingCallContext']
+ guid =uuid.uuid4()
+ query_parameters = urlencode({"callerId": caller_id})
+ callback_uri = f"{CALLBACK_EVENTS_URI}/{guid}?{query_parameters}"
+
+ parsed_url = urlparse(CALLBACK_EVENTS_URI)
+ websocket_url = urlunparse(('wss',parsed_url.netloc,'/ws','', '', ''))
+
+ app.logger.info("callback url: %s", callback_uri)
+ app.logger.info("websocket url: %s", websocket_url)
+
+ media_streaming_options = MediaStreamingOptions(
+ transport_url=websocket_url,
+ transport_type=StreamingTransportType.WEBSOCKET,
+ content_type=MediaStreamingContentType.AUDIO,
+ audio_channel_type=MediaStreamingAudioChannelType.MIXED,
+ start_media_streaming=True,
+ enable_bidirectional=True,
+ audio_format=AudioFormat.PCM24_K_MONO)
+
+ answer_call_result = await acs_client.answer_call(incoming_call_context=incoming_call_context,
+ operation_context="incomingCall",
+ callback_url=callback_uri,
+ media_streaming=media_streaming_options)
+ app.logger.info("Answered call for connection id: %s",
+ answer_call_result.call_connection_id)
+ return Response(status=200)
+
+@app.route('/api/callbacks/', methods=['POST'])
+async def callbacks(contextId):
+ for event in await request.json:
+ # Parsing callback events
+ global call_connection_id
+ event_data = event['data']
+ call_connection_id = event_data["callConnectionId"]
+ app.logger.info(f"Received Event:-> {event['type']}, Correlation Id:-> {event_data['correlationId']}, CallConnectionId:-> {call_connection_id}")
+ if event['type'] == "Microsoft.Communication.CallConnected":
+ call_connection_properties = await acs_client.get_call_connection(call_connection_id).get_call_properties()
+ media_streaming_subscription = call_connection_properties.media_streaming_subscription
+ app.logger.info(f"MediaStreamingSubscription:--> {media_streaming_subscription}")
+ app.logger.info(f"Received CallConnected event for connection id: {call_connection_id}")
+ app.logger.info("CORRELATION ID:--> %s", event_data["correlationId"])
+ app.logger.info("CALL CONNECTION ID:--> %s", event_data["callConnectionId"])
+ elif event['type'] == "Microsoft.Communication.MediaStreamingStarted":
+ app.logger.info(f"Media streaming content type:--> {event_data['mediaStreamingUpdate']['contentType']}")
+ app.logger.info(f"Media streaming status:--> {event_data['mediaStreamingUpdate']['mediaStreamingStatus']}")
+ app.logger.info(f"Media streaming status details:--> {event_data['mediaStreamingUpdate']['mediaStreamingStatusDetails']}")
+ elif event['type'] == "Microsoft.Communication.MediaStreamingStopped":
+ app.logger.info(f"Media streaming content type:--> {event_data['mediaStreamingUpdate']['contentType']}")
+ app.logger.info(f"Media streaming status:--> {event_data['mediaStreamingUpdate']['mediaStreamingStatus']}")
+ app.logger.info(f"Media streaming status details:--> {event_data['mediaStreamingUpdate']['mediaStreamingStatusDetails']}")
+ elif event['type'] == "Microsoft.Communication.MediaStreamingFailed":
+ app.logger.info(f"Code:->{event_data['resultInformation']['code']}, Subcode:-> {event_data['resultInformation']['subCode']}")
+ app.logger.info(f"Message:->{event_data['resultInformation']['message']}")
+ elif event['type'] == "Microsoft.Communication.CallDisconnected":
+ pass
+ return Response(status=200)
+
+# WebSocket.
+@app.websocket('/ws')
+async def ws():
+ handler = OpenAIRTHandler()
+ print("Client connected to WebSocket")
+ await handler.init_incoming_websocket(websocket)
+ await handler.start_client()
+ while websocket:
+ try:
+ # Receive data from the client
+ data = await websocket.receive()
+ await handler.acs_to_oai(data)
+ await handler.send_welcome()
+ except Exception as e:
+ print(f"WebSocket connection closed: {e}")
+ break
+
+@app.route('/')
+def home():
+ return 'Hello ACS CallAutomation!'
+
+if __name__ == '__main__':
+ app.logger.setLevel(INFO)
+ app.run(port=8000)
diff --git a/callautomation-azure-openai-voice/readme.md b/callautomation-azure-openai-voice/readme.md
new file mode 100644
index 0000000..0df4c77
--- /dev/null
+++ b/callautomation-azure-openai-voice/readme.md
@@ -0,0 +1,59 @@
+|page_type| languages |products
+|---|-----------------------------------------|---|
+|sample| || azure | azure-communication-services |
|
+
+# Call Automation - Quick Start Sample
+
+This is a sample application demonstrated during Microsoft Ignite 2024. It highlights an integration of Azure Communication Services with Azure OpenAI Service to enable intelligent conversational agents.
+
+## 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 get inbound calls. NB: phone numbers are not available in free subscriptions.
+- [Python](https://www.python.org/downloads/) 3.7 or above.
+- An Azure OpenAI Resource and Deployed Model. See [instructions](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?pivots=web-portal).
+
+## 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-azure-openai-voice` 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 8000
+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. `CALLBACK_URI_HOST`: Base url of the app. (For local development use dev tunnel url)
+
+Open `azureOpenAIService.py` file to configure the following settings
+
+1. `AZURE_OPENAI_SERVICE_ENDPOINT`: Azure Open AI service endpoint
+2. `AZURE_OPENAI_SERVICE_KEY`: Azure Open AI service key
+3. `AZURE_OPENAI_DEPLOYMENT_MODEL_NAME`: Azure Open AI deployment name
+
+## Run app locally
+
+1. Navigate to `callautomation-azure-openai-voice` 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:8000/`or your dev tunnel url.
+3. Register an EventGrid Webhook for the IncomingCall(`https:///api/incomingCall`) event that points to your devtunnel URI. Instructions [here](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/incoming-call-notification).
+
+Once that's completed you should have a running application. The best way to test this is to place a call to your ACS phone number and talk to your intelligent agent.
diff --git a/callautomation-azure-openai-voice/requirements.txt b/callautomation-azure-openai-voice/requirements.txt
new file mode 100644
index 0000000..5b45f5b
--- /dev/null
+++ b/callautomation-azure-openai-voice/requirements.txt
@@ -0,0 +1,5 @@
+Quart>=0.19.6
+azure-eventgrid==4.11.0
+aiohttp>= 3.11.9
+azure-communication-callautomation==1.4.0
+openai[realtime]
\ No newline at end of file
diff --git a/callautomation-connect-room/config.py b/callautomation-connect-room/config.py
new file mode 100644
index 0000000..b071033
--- /dev/null
+++ b/callautomation-connect-room/config.py
@@ -0,0 +1,8 @@
+class Config:
+ PORT = 8080
+ CONNECTION_STRING = ""
+ ACS_RESOURCE_PHONE_NUMBER = ""
+ TARGET_PHONE_NUMBER = "<+1XXXXXXXXXX>"
+ CALLBACK_URI = ""
+ COGNITIVE_SERVICES_ENDPOINT = ""
+
\ No newline at end of file
diff --git a/callautomation-connect-room/data/callingRoomQuickstart.png b/callautomation-connect-room/data/callingRoomQuickstart.png
new file mode 100644
index 0000000..a1b2435
Binary files /dev/null and b/callautomation-connect-room/data/callingRoomQuickstart.png differ
diff --git a/callautomation-connect-room/data/connectCall.png b/callautomation-connect-room/data/connectCall.png
new file mode 100644
index 0000000..98cbc39
Binary files /dev/null and b/callautomation-connect-room/data/connectCall.png differ
diff --git a/callautomation-connect-room/data/createRoom.png b/callautomation-connect-room/data/createRoom.png
new file mode 100644
index 0000000..cfc3abf
Binary files /dev/null and b/callautomation-connect-room/data/createRoom.png differ
diff --git a/callautomation-connect-room/data/joinRoomCall.png b/callautomation-connect-room/data/joinRoomCall.png
new file mode 100644
index 0000000..dacaa68
Binary files /dev/null and b/callautomation-connect-room/data/joinRoomCall.png differ
diff --git a/callautomation-connect-room/data/roomId.png b/callautomation-connect-room/data/roomId.png
new file mode 100644
index 0000000..60e81e1
Binary files /dev/null and b/callautomation-connect-room/data/roomId.png differ
diff --git a/callautomation-connect-room/data/tokens.png b/callautomation-connect-room/data/tokens.png
new file mode 100644
index 0000000..dfd1df6
Binary files /dev/null and b/callautomation-connect-room/data/tokens.png differ
diff --git a/callautomation-connect-room/main.py b/callautomation-connect-room/main.py
new file mode 100644
index 0000000..17f6cf6
--- /dev/null
+++ b/callautomation-connect-room/main.py
@@ -0,0 +1,232 @@
+import time
+from quart import Quart, render_template, jsonify, request, redirect
+from azure.core.exceptions import HttpResponseError
+from datetime import datetime, timezone, timedelta
+from azure.communication.callautomation import (
+ PhoneNumberIdentifier,
+ TextSource
+)
+from azure.communication.identity import (
+ CommunicationUserIdentifier,
+ CommunicationIdentityClient
+)
+from azure.communication.callautomation.aio import (
+ CallAutomationClient
+ )
+from azure.communication.rooms import (
+ RoomsClient,
+ RoomParticipant,
+ ParticipantRole
+)
+
+from logging import INFO
+import asyncio
+from config import Config
+
+# Initialize Quart app
+app = Quart(__name__)
+
+# Use config values from config.py
+PORT = Config.PORT
+CONNECTION_STRING = Config.CONNECTION_STRING
+ACS_RESOURCE_PHONE_NUMBER = Config.ACS_RESOURCE_PHONE_NUMBER
+TARGET_PHONE_NUMBER = Config.TARGET_PHONE_NUMBER
+CALLBACK_URI = Config.CALLBACK_URI
+COGNITIVE_SERVICES_ENDPOINT = Config.COGNITIVE_SERVICES_ENDPOINT
+
+# Initialize variables
+acs_client = None
+call_connection = None
+call_connection_id = None
+server_call_id = None
+room_id = None
+
+# Global variables to store room and user details
+room_details = None
+
+# Initialize ACS Client
+async def create_acs_client():
+ global acs_client
+ acs_client = CallAutomationClient.from_connection_string(CONNECTION_STRING)
+ print("Initialized ACS Client.")
+
+# Create Room
+async def create_room():
+ identity_client = CommunicationIdentityClient.from_connection_string(CONNECTION_STRING)
+ app.logger.info("Test")
+ # Correct unpacking of the returned tuple
+ user1, token1 = identity_client.create_user_and_token(["voip"])
+ user2, token2 = identity_client.create_user_and_token(["voip"])
+
+ communication_user_id1 = user1.properties['id']
+ communication_user_id2 = user2.properties['id']
+ # Now you can access the 'user' and 'token' separately
+ print(f"Presenter: {communication_user_id1}, Token: {token1.token}")
+ print(f"Attendee: {communication_user_id2}, Token: {token2.token}")
+
+ rooms_client = RoomsClient.from_connection_string(CONNECTION_STRING)
+
+ valid_from = datetime.now(timezone.utc)
+ valid_until = valid_from + timedelta(weeks=7)
+ # Create participants
+ participants = [
+ RoomParticipant(
+ communication_identifier=CommunicationUserIdentifier(communication_user_id1),
+ role=ParticipantRole.PRESENTER # Presenter role
+ ),
+ RoomParticipant(
+ communication_identifier=CommunicationUserIdentifier(communication_user_id2),
+ role=ParticipantRole.CONSUMER # Attendee role
+ )
+ ]
+
+ try:
+ create_room_response = rooms_client.create_room(
+ valid_from=valid_from,
+ valid_until=valid_until,
+ participants=participants,
+ pstn_dial_out_enabled=True
+ )
+ except HttpResponseError as ex:
+ print(ex)
+
+ global room_id
+ room_id = create_room_response.id
+ print(f"Room created with ID: {room_id}")
+
+ room_details = {
+ "room_id": create_room_response.id,
+ "presenter_id": communication_user_id1,
+ "presenter_token": token1.token,
+ "attendee_id": communication_user_id2,
+ "attendee_token": token2.token
+ }
+ return room_details
+# Connect call to the room
+async def connect_call():
+ global call_connection_id
+ if room_id:
+# Use CALLBACK_URI from the config
+ callback_uri = CALLBACK_URI + "/api/callbacks"
+ app.logger.info(f"Callback URL: {callback_uri}")
+ app.logger.info(f"Room ID: {room_id}")
+
+ response = await acs_client.connect_call(
+ room_id=room_id,
+ callback_url=callback_uri,
+ cognitive_services_endpoint=COGNITIVE_SERVICES_ENDPOINT,
+ operation_context="connectCallContext")
+ print("Call connection initiated.")
+ app.logger.info(f"Connect request correlation id: {response.correlation_id}")
+ else:
+ print("Room ID is empty or room not available.")
+
+# Add PSTN participant to the call
+async def add_pstn_participant():
+ if call_connection_id:
+ target = PhoneNumberIdentifier(TARGET_PHONE_NUMBER)
+ source_caller_id_number = PhoneNumberIdentifier(ACS_RESOURCE_PHONE_NUMBER)
+ app.logger.info("source_caller_id_number: %s", source_caller_id_number)
+
+ add_participant_result= await call_connection.add_participant(target_participant=target,
+ source_caller_id_number=source_caller_id_number,
+ operation_context=None,
+ invitation_timeout=15)
+ app.logger.info("Add Translator to the call: %s", add_participant_result.invitation_id)
+
+ print(f"Adding PSTN participant with invitation ID: {add_participant_result.invitation_id}")
+ else:
+ print("Call connection ID is empty or call not active.")
+
+# Hangup the call
+async def hang_up_call():
+ if call_connection:
+ await call_connection.hang_up(True)
+ print("Call hung up.")
+
+async def handle_play():
+ play_source = TextSource(text="Hello, welcome to connect room contoso app.", voice_name="en-US-NancyNeural")
+ if call_connection:
+ await call_connection.play_media_to_all(play_source)
+
+# Routes
+@app.route('/')
+async def home():
+ global room_details
+# Check if room details already exist
+ if not room_details:
+ # Create the ACS client and room only if room is not created yet
+ await create_acs_client()
+ room_details = await create_room()
+
+ # Render the page with the room details
+ return await render_template('index.html', details=room_details)
+
+@app.route('/connectCall', methods=['GET'])
+async def connect_call_route():
+ await connect_call()
+ return redirect('/')
+
+@app.route('/addParticipant', methods=['GET'])
+async def add_participant_route():
+ await add_pstn_participant()
+ return redirect('/')
+
+@app.route('/hangup', methods=['GET'])
+async def hangup_route():
+ await hang_up_call()
+ return redirect('/')
+
+@app.route('/playMedia', methods=['GET'])
+async def play_media_route():
+ await handle_play()
+ return redirect('/')
+
+@app.route('/api/callbacks', methods=['POST'])
+async def handle_callbacks():
+ try:
+ global call_connection_id, server_call_id, call_connection
+
+ # Extract the first event from the request body
+ events = await request.json
+ event = events[0] # Assumes at least one event is present
+ event_data = event['data']
+ # Handle specific event types
+ if event['type'] == "Microsoft.Communication.CallConnected":
+ app.logger.info("Received CallConnected event")
+ app.logger.info(f"Correlation ID: {event_data['correlationId']}")
+ # Extract necessary details
+ call_connection_id = event_data['callConnectionId']
+ server_call_id = event_data['serverCallId']
+ call_connection = acs_client.get_call_connection(call_connection_id)
+
+ elif event['type'] == "Microsoft.Communication.AddParticipantSucceeded":
+ app.logger.info("Received AddParticipantSucceeded event")
+
+ elif event['type'] == "Microsoft.Communication.AddParticipantFailed":
+ result_info = event_data['resultInformation']
+ app.logger.info("Received AddParticipantFailed event")
+ app.logger.info(f"Code: {result_info['code']}, Subcode: {result_info['subCode']}")
+ app.logger.info(f"Message: {result_info['message']}")
+ elif event['type'] == "Microsoft.Communication.PlayCompleted":
+ app.logger.info("Received PlayCompleted event")
+ elif event['type'] == "Microsoft.Communication.PlayFailed":
+ result_info = event_data['resultInformation']
+ app.logger.info("Received PlayFailed event")
+ app.logger.info(f"Code: {result_info['code']}, Subcode: {result_info['subCode']}")
+ app.logger.info(f"Message: {result_info['message']}")
+ elif event['type'] == "Microsoft.Communication.CallDisconnected":
+ app.logger.info("Received CallDisconnected event")
+ app.logger.info(f"Correlation ID: {event_data['correlationId']}")
+
+ # Respond with a success status
+ return jsonify({"status": "OK"}), 200
+
+ except Exception as e:
+ app.logger.error(f"Error processing callback: {e}")
+ return jsonify({"error": "Internal server error"}), 500
+
+if __name__ == '__main__':
+ app.logger.setLevel(INFO)
+ app.run(port=PORT)
+
diff --git a/callautomation-connect-room/readme.md b/callautomation-connect-room/readme.md
new file mode 100644
index 0000000..4934a93
--- /dev/null
+++ b/callautomation-connect-room/readme.md
@@ -0,0 +1,76 @@
+|page_type| languages |products
+|---|-----------------------------------------|---|
+|sample| || azure | azure-communication-services |
|
+
+# Connect to a room call using Call Automation SDK
+
+In this quickstart sample, we cover how you can use Call Automation SDK to connect to an active Azure Communication Services (ACS) Rooms call with a connect endpoint.
+This involves creating a room call with room id and users and enabling PSTN dial out to add PSTN participant(s).
+
+## Prerequisites
+
+- Create an Azure account with an active subscription. For details, see [Create an account for free](https://azure.microsoft.com/free/)
+- 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.
+- An Calling-enabled telephone number. [Get a phone number](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/telephony/get-phone-number?tabs=windows&pivots=platform-azp).
+- 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).
+- [Python](https://www.python.org/downloads/) 3.7 or above.
+
+## 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-connect-room` folder and open `main.py` file.
+
+## Before running calling rooms quickstart
+1. To initiate rooms call with room id https://github.com/Azure-Samples/communication-services-javascript-quickstarts/tree/main/calling-rooms-quickstart
+2. cd into the `calling-rooms-quickstart` folder.
+3. From the root of the above folder, and with node installed, run `npm install`
+4. to run sample `npx webpack serve --config webpack.config.js`
+
+### Setup the Python environment
+
+[Optional] Create and activate python virtual environment and install required packages using following command
+```
+python -m venv venv
+venv\Scripts\activate
+```
+Install the required packages using the 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 `config.py` file to configure the following settings
+
+1. - `CALLBACK_URI`: Ngrok url for the server port (in this example port 8080)
+2. - `CONNECTION_STRING`: Azure Communication Service resource's connection string.
+3. - `ACS_RESOURCE_PHONE_NUMBER`: Acs Phone Number
+4. - `TARGET_PHONE_NUMBER`: Agent Phone Number to add into the call
+5. - `COGNITIVE_SERVICES_ENDPOINT`: Cognitive service endpoint.
+
+## Run app locally
+
+1. Navigate to `callautomation-connect-room` 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 ngrok url which points to 8080 port.
+3. To connect rooms call, click on the `Connect a call!` button or make a Http get request to https:///connectCall
+
+### Creating and connecting to room call.
+
+1. Navigate to `http://localhost:8080/` or devtunnel url to create users and room id 
+2. Open two tabs for Presenter and attendee 
+3. Copy tokens for presenter and attendee from 
+4. Initialize call agent with tokens for both presenter and attendee.
+5. Take room id  and initiate rooms call for both users. 
+6. Connect room call with callautomation connect call endpoint. 
+
diff --git a/callautomation-connect-room/requirements.txt b/callautomation-connect-room/requirements.txt
new file mode 100644
index 0000000..a5bf421
--- /dev/null
+++ b/callautomation-connect-room/requirements.txt
@@ -0,0 +1,8 @@
+Quart>=0.19.6
+python-dotenv==0.21.0
+azure-eventgrid==4.11.0
+aiohttp>= 3.11.9
+azure-communication-callautomation==1.3.0
+azure-communication-identity==1.3.1
+azure-communication-rooms==1.1.1
+
diff --git a/callautomation-connect-room/templates/index.html b/callautomation-connect-room/templates/index.html
new file mode 100644
index 0000000..2640018
--- /dev/null
+++ b/callautomation-connect-room/templates/index.html
@@ -0,0 +1,41 @@
+
+
+
+
+ Azure Communication Services Quickstart
+
+
+
+ Azure Communication Services
+ Connect Rooms Call Quickstart
+
+
+
+
+
\ No newline at end of file
diff --git a/callautomation-live-transcription/main.py b/callautomation-live-transcription/main.py
new file mode 100644
index 0000000..d01cffd
--- /dev/null
+++ b/callautomation-live-transcription/main.py
@@ -0,0 +1,372 @@
+
+import ast
+import uuid
+import os
+from pathlib import Path
+from urllib.parse import urlencode, urljoin, urlparse, urlunparse
+from azure.eventgrid import EventGridEvent, SystemEventNames
+import requests
+from quart import Quart, Response, request, json, redirect, websocket, render_template
+import json
+from logging import INFO
+import re
+from azure.communication.callautomation import (
+ PhoneNumberIdentifier,
+ RecognizeInputType,
+ TextSource,
+ # TranscriptionConfiguration,
+ StreamingTransportType,
+ ServerCallLocator,
+ TranscriptionOptions,
+ RecordingContent,
+ RecordingChannel,
+ RecordingFormat
+ )
+from azure.communication.callautomation.aio import (
+ CallAutomationClient
+ )
+from azure.core.messaging import CloudEvent
+import time
+import asyncio
+import json
+from azure.communication.callautomation._shared.models import identifier_from_raw_id
+from transcriptionDataHandler import process_websocket_message_async
+# import openai
+
+# from openai.api_resources import (
+# ChatCompletion
+# )
+
+# Your ACS resource connection string
+ACS_CONNECTION_STRING = ""
+
+# Cognitive service endpoint
+COGNITIVE_SERVICE_ENDPOINT=""
+
+# Acs Phone Number
+ACS_PHONE_NUMBER=""
+
+# Transcription Locale
+LOCALE=""
+
+# Agent Phone Number
+AGENT_PHONE_NUMBER=""
+
+# Callback events URI to handle callback events.
+CALLBACK_URI_HOST = ""
+CALLBACK_EVENTS_URI = CALLBACK_URI_HOST + "/api/callbacks"
+
+HELP_IVR_PROMPT = "Welcome to the Contoso Utilities. To access your account, we need to verify your identity. Please enter your date of birth in the format DDMMYYYY using the keypad on your phone. Once we’ve validated your identity we will connect you to the next available agent. Please note this call will be recorded!"
+ADD_AGENT_PROMPT = "Thank you for verifying your identity. We are now connecting you to the next available agent. Please hold the line and we will be with you shortly. Thank you for your patience."
+INCORRECT_DOB_PROMPT = "Sorry, we were unable to verify your identity based on the date of birth you entered. Please try again. Remember to enter your date of birth in the format DDMMYYYY using the keypad on your phone. Once you've entered your date of birth, press the pound key. Thank you!"
+ADD_PARTICIPANT_FAILURE_PROMPT = "We're sorry, we were unable to connect you to an agent at this time, we will get the next available agent to call you back as soon as possible."
+GOODBYE_PROMPT = "Thank you for calling Contoso Utilities. We hope we were able to assist you today. Goodbye"
+TIMEOUT_SILENCE_PROMPT = "I’m sorry, I didn’t receive any input. Please type your date of birth in the format of DDMMYYYY."
+GOODBYE_CONTEXT = "Goodbye"
+ADD_AGENT_CONTEXT = "AddAgent"
+INCORRECT_DOB_CONTEXT = "IncorrectDob"
+ADD_PARTICIPANT_FAILURE_CONTEXT = "FailedToAddParticipant"
+INCOMING_CALL_CONTEXT = "incomingCallContext"
+
+DOB_REGEX = r"^(0[1-9]|[12][0-9]|3[01])(0[1-9]|1[012])[12][0-9]{3}$"
+
+call_automation_client = CallAutomationClient.from_connection_string(ACS_CONNECTION_STRING)
+
+recording_id = None
+recording_chunks_location = []
+recording_callback_url = None
+max_retry = 2
+words_to_numbers = {
+ 'one': 1,
+ 'two': 2,
+ 'three': 3,
+ 'four': 4,
+ 'five': 5,
+ 'six': 6,
+ 'seven': 7,
+ 'eight': 8,
+ 'nine': 9,
+ 'zero': 0
+ }
+
+TEMPLATE_FILES_PATH = "template"
+app = Quart(__name__,
+ template_folder=TEMPLATE_FILES_PATH)
+
+async def handle_recognize(text_to_play,caller_id,call_connection_id,context=""):
+ play_source = TextSource(text=text_to_play, voice_name="en-US-NancyNeural")
+ connection_client = call_automation_client.get_call_connection(call_connection_id)
+ try:
+ recognize_result = await connection_client.start_recognizing_media(
+ dtmf_max_tones_to_collect=8,
+ input_type=RecognizeInputType.DTMF,
+ target_participant=PhoneNumberIdentifier(caller_id),
+ end_silence_timeout=15,
+ dtmf_inter_tone_timeout=5,
+ play_prompt=play_source,
+ operation_context=context)
+ app.logger.info("handle_recognize : data=%s",recognize_result)
+ except Exception as ex:
+ app.logger.info("Error in recognize: %s", ex)
+
+async def handle_play(call_connection_id, text_to_play, context):
+ play_source = TextSource(text=text_to_play, voice_name= "en-US-NancyNeural")
+ await call_automation_client.get_call_connection(call_connection_id).play_media_to_all(play_source,operation_context=context)
+
+async def handle_hangup(call_connection_id):
+ await call_automation_client.get_call_connection(call_connection_id).hang_up(is_for_everyone=True)
+
+@app.route("/api/incomingCall", methods=['POST'])
+async def incoming_call_handler():
+ app.logger.info("Received incoming call event.")
+ try:
+ for event_dict in await request.json:
+ event = EventGridEvent.from_dict(event_dict)
+ app.logger.info("incoming event data --> %s", event.data)
+ if event.event_type == SystemEventNames.EventGridSubscriptionValidationEventName:
+ app.logger.info("Validating subscription")
+ validation_code = event.data['validationCode']
+ return Response(response=json.dumps({"validationResponse": validation_code}), status=200)
+
+ if event.event_type == "Microsoft.Communication.IncomingCall":
+ app.logger.info("Incoming call received: data=%s", event.data)
+ if event.data['from']['kind'] =="phoneNumber":
+ caller_id = event.data['from']["phoneNumber"]["value"]
+ else :
+ caller_id = event.data['from']['rawId']
+ app.logger.info("incoming call handler caller id: %s",
+ caller_id)
+ incoming_call_context = event.data['incomingCallContext']
+ guid = uuid.uuid4()
+ callback_uri = f"{CALLBACK_EVENTS_URI}/{guid}?callerId={caller_id}"
+ websocket_url = urlunparse(("wss", urlparse(CALLBACK_URI_HOST).netloc, "/ws", "", "", ""))
+ global recording_callback_url
+ recording_callback_url = callback_uri
+ transcription_config = TranscriptionOptions(
+ transport_url=websocket_url,
+ transport_type=StreamingTransportType.WEBSOCKET,
+ locale=LOCALE,
+ start_transcription=True
+ )
+
+ try:
+ answer_call_result = await call_automation_client.answer_call(
+ incoming_call_context=incoming_call_context,
+ transcription=transcription_config,
+ cognitive_services_endpoint=COGNITIVE_SERVICE_ENDPOINT,
+ callback_url=callback_uri
+ )
+ app.logger.info(f"Call answered, connection ID: {answer_call_result.call_connection_id}")
+ except Exception as e:
+ app.logger.error(f"Failed to answer call: {e}")
+ return Response(status=500)
+ return Response(status=200)
+ except Exception as ex:
+ app.logger.error(f"Error handling incoming call: {ex}")
+ return Response(status=500)
+
+@app.route("/api/callbacks/", methods=["POST"])
+async def handle_callback(contextId):
+ try:
+ global caller_id , call_connection_id
+ # app.logger.info("Request Json: %s", request.json)
+ for event_dict in await request.json:
+ event = CloudEvent.from_dict(event_dict)
+ call_connection_id = event.data['callConnectionId']
+
+ app.logger.info("%s event received for call connection id: %s, correlation id: %s",
+ event.type, call_connection_id, event.data["correlationId"])
+ caller_id = request.args.get("callerId").strip()
+ if "+" not in caller_id:
+ caller_id="+".strip()+caller_id.strip()
+
+ app.logger.info("call connected : data=%s", event.data)
+ if event.type == "Microsoft.Communication.CallConnected":
+ recording_result = await call_automation_client.start_recording(
+ server_call_id=event.data["serverCallId"],
+ recording_content_type=RecordingContent.AUDIO_VIDEO,
+ recording_channel_type=RecordingChannel.MIXED,
+ recording_format_type=RecordingFormat.MP4,
+ recording_state_callback_url=recording_callback_url,
+ pause_on_start=True
+ )
+ global recording_id
+ recording_id=recording_result.recording_id
+ global call_properties
+ call_properties = await call_automation_client.get_call_connection(call_connection_id).get_call_properties()
+ app.logger.info("Transcription subscription--->=%s", call_properties.transcription_subscription)
+
+ elif event.type == "Microsoft.Communication.PlayStarted":
+ app.logger.info("Received PlayStarted event.")
+ elif event.type == "Microsoft.Communication.PlayCompleted":
+ context=event.data['operationContext']
+ app.logger.info("Play completed: context=%s", event.data['operationContext'])
+ if context == ADD_AGENT_CONTEXT:
+ app.logger.info("Add agent to the call: %s", ADD_AGENT_CONTEXT)
+ #Add agent
+ target = PhoneNumberIdentifier(AGENT_PHONE_NUMBER)
+ source_caller_id_number = PhoneNumberIdentifier(ACS_PHONE_NUMBER)
+ app.logger.info("source_caller_id_number: %s", source_caller_id_number)
+ call_connection = call_automation_client.get_call_connection(call_connection_id)
+ add_participant_result= await call_connection.add_participant(target_participant=target,
+ source_caller_id_number=source_caller_id_number,
+ operation_context=None,
+ invitation_timeout=15)
+ app.logger.info("Add agent to the call: %s", add_participant_result.invitation_id)
+ elif context == GOODBYE_CONTEXT or context == ADD_PARTICIPANT_FAILURE_CONTEXT:
+ await stop_transcription_and_recording(call_connection_id, recording_id=recording_id)
+ await handle_hangup(call_connection_id=call_connection_id)
+ elif event.type == "Microsoft.Communication.RecognizeCompleted":
+ app.logger.info("Recognize completed: data=%s", event.data)
+ if event.data['recognitionType'] == "dtmf":
+ dtmf_tones = event.data['dtmfResult']['tones'];
+ app.logger.info("Recognition completed, dtmf tones =%s", dtmf_tones)
+ global words_to_numbers
+ numbers = "".join(str(words_to_numbers [x]) for x in dtmf_tones)
+ regex = re.compile(DOB_REGEX)
+ match = regex.search(numbers)
+ if match:
+ await resume_transcription_and_recording(call_connection_id, recording_id)
+ else:
+ await handle_recognize(INCORRECT_DOB_PROMPT, caller_id, call_connection_id, INCORRECT_DOB_CONTEXT)
+ elif event.type == "Microsoft.Communication.RecognizeFailed":
+ resultInformation = event.data['resultInformation']
+ app.logger.info("Recognize failed event received: message=%s, sub code=%s", resultInformation['message'],resultInformation['subCode'])
+ reasonCode = resultInformation['subCode']
+ context=event.data['operationContext']
+ global max_retry
+ if reasonCode == 8510 and 0 < max_retry:
+ await handle_recognize(TIMEOUT_SILENCE_PROMPT,caller_id,call_connection_id, context="retryContext")
+ max_retry -= 1
+ else:
+ await handle_play(call_connection_id=call_connection_id,text_to_play=GOODBYE_PROMPT, context=GOODBYE_CONTEXT)
+ elif event.type == "Microsoft.Communication.AddParticipantFailed":
+ resultInformation = event.data['resultInformation']
+ app.logger.info("Received Add Participants Failed message=%s, sub code=%s",resultInformation['message'],resultInformation['subCode'])
+ await handle_play(call_connection_id=call_connection_id,text_to_play=ADD_PARTICIPANT_FAILURE_PROMPT, context=ADD_PARTICIPANT_FAILURE_CONTEXT)
+ elif event.type == "Microsoft.Communication.RecordingStateChanged":
+ app.logger.info("Received RecordingStateChanged event.")
+ app.logger.info(event.data['state'])
+ elif event.type == "Microsoft.Communication.TranscriptionStarted":
+ app.logger.info("Received TranscriptionStarted event.")
+ operation_context = None
+ if 'operationContext' in event.data:
+ operation_context = event.data['operationContext']
+
+ if operation_context is None:
+ await call_automation_client.get_call_connection(event.data['callConnectionId']).stop_transcription(operation_context="nextRecognizeContext")
+ elif operation_context is not None and operation_context == 'StartTranscriptionContext':
+ await handle_play(event.data['callConnectionId'], ADD_AGENT_PROMPT, ADD_AGENT_CONTEXT)
+
+ elif event.type == "Microsoft.Communication.TranscriptionStopped":
+ app.logger.info("Received TranscriptionStopped event.")
+ operation_context = None
+ if 'operationContext' in event.data:
+ operation_context = event.data['operationContext']
+
+ if operation_context is not None and operation_context == 'nextRecognizeContext':
+ await handle_recognize(HELP_IVR_PROMPT,caller_id,call_connection_id,context="hellocontext")
+
+ elif event.type == "Microsoft.Communication.TranscriptionUpdated":
+ app.logger.info("Received TranscriptionUpdated event.")
+ transcriptionUpdate = event.data['transcriptionUpdate']
+ app.logger.info(transcriptionUpdate["transcriptionStatus"])
+ app.logger.info(transcriptionUpdate["transcriptionStatusDetails"])
+ elif event.type == "Microsoft.Communication.TranscriptionFailed":
+ app.logger.info("Received TranscriptionFailed event.")
+ resultInformation = event.data['resultInformation']
+ app.logger.info("Encountered error during Transcription, message=%s, code=%s, subCode=%s",
+ resultInformation['message'],
+ resultInformation['code'],
+ resultInformation['subCode'])
+ return Response(status=200)
+ except Exception as ex:
+ app.logger.info("error in event handling")
+
+@app.route('/api/recordingFileStatus', methods=['POST'])
+async def recording_file_status():
+ try:
+ for event_dict in await request.json:
+ event = EventGridEvent.from_dict(event_dict)
+ if event.event_type == SystemEventNames.EventGridSubscriptionValidationEventName:
+ code = event.data['validationCode']
+ if code:
+ data = {"validationResponse": code}
+ app.logger.info("Successfully Subscribed EventGrid.ValidationEvent --> " + str(data))
+ return Response(response=str(data), status=200)
+
+ if event.event_type == SystemEventNames.AcsRecordingFileStatusUpdatedEventName:
+ acs_recording_file_status_updated_event_data = event.data
+ acs_recording_chunk_info_properties = acs_recording_file_status_updated_event_data['recordingStorageInfo']['recordingChunks'][0]
+ app.logger.info("acsRecordingChunkInfoProperties response data --> " + str(acs_recording_chunk_info_properties))
+ global content_location
+ content_location = acs_recording_chunk_info_properties['contentLocation']
+ return Response(response="Ok")
+
+ except Exception as ex:
+ app.logger.error( "Failed to get recording file")
+ return Response(response='Failed to get recording file', status=400)
+
+@app.route('/download')
+async def download_recording():
+ try:
+ app.logger.info("Content location : %s", content_location)
+ downloads_folder = str(Path.home() / "Downloads")
+ file_path = os.path.join(downloads_folder, "Recording_File.mp4")
+
+ recording_data = await call_automation_client.download_recording(content_location)
+ with open(file_path, "wb") as binary_file:
+ binary_file.write(await recording_data.read())
+ return redirect("/")
+ except Exception as ex:
+ app.logger.info("Failed to download recording --> " + str(ex))
+ return Response(text=str(ex), status=500)
+
+async def initiate_transcription(call_connection_id):
+ app.logger.info("initiate_transcription is called %s", call_connection_id)
+ call_connection = call_automation_client.get_call_connection(call_connection_id)
+ await call_connection.start_transcription(locale=LOCALE, operation_context="StartTranscriptionContext")
+ app.logger.info("Starting the transcription")
+
+async def stop_transcription_and_recording(call_connection_id, recording_id):
+ app.logger.info("stop_transcription_and_recording method triggered.")
+ call_properties = await call_automation_client.get_call_connection(call_connection_id).get_call_properties()
+ recording_properties = await call_automation_client.get_recording_properties(recording_id)
+ if call_properties.transcription_subscription.state == 'active':
+ await call_automation_client.get_call_connection(call_connection_id).stop_transcription()
+ if recording_properties.recording_state == "active":
+ await call_automation_client.stop_recording(recording_id=recording_id)
+
+async def resume_transcription_and_recording(call_connection_id, recording_id):
+ await initiate_transcription(call_connection_id)
+ app.logger.info("Transcription re initiated.")
+
+ await call_automation_client.resume_recording(recording_id)
+ app.logger.info(f"Recording resumed. RecordingId: {recording_id}")
+
+ # WebSocket.
+@app.websocket('/ws')
+async def ws():
+ print("Client connected to WebSocket")
+ try:
+ while True:
+ try:
+ # Receive data from the client
+ message = await websocket.receive()
+ await process_websocket_message_async(message)
+ except Exception as e:
+ print(f"Error while receiving message: {e}")
+ break # Close connection on error
+ except Exception as e:
+ print(f"WebSocket connection closed: {e}")
+ finally:
+ # Any cleanup or final logs can go here
+ print("WebSocket connection closed")
+
+@app.route('/')
+async def index_handler():
+ return await render_template("index.html")
+
+if __name__ == '__main__':
+ app.logger.setLevel(INFO)
+ app.run(port=8080)
diff --git a/callautomation-live-transcription/readme.md b/callautomation-live-transcription/readme.md
new file mode 100644
index 0000000..2fe39fb
--- /dev/null
+++ b/callautomation-live-transcription/readme.md
@@ -0,0 +1,65 @@
+|page_type| languages |products
+|---|-----------------------------------------|---|
+|sample| || azure | azure-communication-services |
|
+
+# Call Automation - Quick Start Sample
+
+This sample application shows how the Azure Communication Services - Call Automation SDK can be used generate the live transcription between PSTN calls.
+It accepts an incoming call from a phone number, performs DTMF recognition, and transfer the call to agent. You can see the live transcription in websocket during the conversation between agent and user
+This sample application is also capable of making multiple concurrent inbound calls. The application is a web-based application built on Python.
+
+## Prerequisites
+
+- Create an Azure account with an active subscription. For details, see [Create an account for free](https://azure.microsoft.com/free/)
+- 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.
+- An Calling-enabled telephone number. [Get a phone number](https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/telephony/get-phone-number?tabs=windows&pivots=platform-azp).
+- 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).
+- An Azure OpenAI Resource and Deployed Model. See [instructions](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal).
+- [Python](https://www.python.org/downloads/) 3.7 or above.
+
+## 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-openai-sample` folder and open `main.py` file.
+
+### Setup the Python environment
+
+[Optional] Create and activate python virtual environment and install required packages using following command
+```
+python -m venv venv
+venv\Scripts\activate
+```
+Install the required packages using the 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. - `CALLBACK_URI_HOST`: Ngrok url for the server port (in this example port 8080)
+2. - `COGNITIVE_SERVICE_ENDPOINT`: The Cognitive Services endpoint
+3. - `ACS_CONNECTION_STRING`: Azure Communication Service resource's connection string.
+4. - `ACS_PHONE_NUMBER`: Acs Phone Number
+5. - `LOCALE`: Transcription locale
+6. - `AGENT_PHONE_NUMBER`: Agent Phone Number to add into the call
+
+## Run app locally
+
+1. Navigate to `callautomation-live-transcription` 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 ngrok url which points to 8080 port.
+4. Register an EventGrid Webhook for the IncomingCall(`https:///api/incomingCall`) and for Recording File Status(`https:///api/recordingFileStatus`) Event that points to your devtunnel URI. Instructions [here](https://learn.microsoft.com/en-us/azure/communication-services/concepts/call-automation/incoming-call-notification).
+
+Once that's completed you should have a running application. The best way to test this is to place a call to your ACS phone number and talk to your intelligent agent.
diff --git a/callautomation-live-transcription/requirements.txt b/callautomation-live-transcription/requirements.txt
new file mode 100644
index 0000000..a01c059
--- /dev/null
+++ b/callautomation-live-transcription/requirements.txt
@@ -0,0 +1,4 @@
+Quart>=0.19.6
+azure-eventgrid==4.11.0
+aiohttp>= 3.11.9
+azure-communication-callautomation==1.4.0
diff --git a/callautomation-live-transcription/template/index.html b/callautomation-live-transcription/template/index.html
new file mode 100644
index 0000000..149eb42
--- /dev/null
+++ b/callautomation-live-transcription/template/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+ Azure Communication Services Quickstart
+
+
+
+ Azure Communication Services
+ Live Transcription Quickstart
+
+
+
+
\ No newline at end of file
diff --git a/callautomation-live-transcription/transcriptionDataHandler.py b/callautomation-live-transcription/transcriptionDataHandler.py
new file mode 100644
index 0000000..6664fc5
--- /dev/null
+++ b/callautomation-live-transcription/transcriptionDataHandler.py
@@ -0,0 +1,31 @@
+import json
+from azure.communication.callautomation._shared.models import identifier_from_raw_id
+
+async def process_websocket_message_async(message):
+ print("Client connected")
+ json_object = json.loads(message)
+ kind = json_object['kind']
+ print(kind)
+ if kind == 'TranscriptionMetadata':
+ print("Transcription metadata")
+ print("-------------------------")
+ print("Subscription ID:", json_object['transcriptionMetadata']['subscriptionId'])
+ print("Locale:", json_object['transcriptionMetadata']['locale'])
+ print("Call Connection ID:", json_object['transcriptionMetadata']['callConnectionId'])
+ print("Correlation ID:", json_object['transcriptionMetadata']['correlationId'])
+ if kind == 'TranscriptionData':
+ participant = identifier_from_raw_id(json_object['transcriptionData']['participantRawID'])
+ word_data_list = json_object['transcriptionData']['words']
+ print("Transcription data")
+ print("-------------------------")
+ print("Text:", json_object['transcriptionData']['text'])
+ print("Format:", json_object['transcriptionData']['format'])
+ print("Confidence:", json_object['transcriptionData']['confidence'])
+ print("Offset:", json_object['transcriptionData']['offset'])
+ print("Duration:", json_object['transcriptionData']['duration'])
+ print("Participant:", participant.raw_id)
+ print("Result Status:", json_object['transcriptionData']['resultStatus'])
+ for word in word_data_list:
+ print("Word:", word['text'])
+ print("Offset:", word['offset'])
+ print("Duration:", word['duration'])
diff --git a/callautomation-openai-sample/main.py b/callautomation-openai-sample/main.py
index 2d6c37a..a06e653 100644
--- a/callautomation-openai-sample/main.py
+++ b/callautomation-openai-sample/main.py
@@ -4,15 +4,18 @@
from urllib.parse import urlencode, urljoin
from azure.eventgrid import EventGridEvent, SystemEventNames
import requests
-from flask import Flask, Response, request, json
+from quart import Quart, Response, request, json
from logging import INFO
import re
from azure.communication.callautomation import (
- CallAutomationClient,
PhoneNumberIdentifier,
RecognizeInputType,
TextSource
)
+
+from azure.communication.callautomation.aio import (
+ CallAutomationClient
+ )
from azure.core.messaging import CloudEvent
import openai
@@ -81,9 +84,9 @@
openai.api_type = 'azure'
openai.api_version = '2023-05-15' # this may change in the future
-app = Flask(__name__)
+app = Quart(__name__)
-def get_chat_completions_async(system_prompt,user_prompt):
+async def get_chat_completions_async(system_prompt,user_prompt):
openai.api_key = AZURE_OPENAI_SERVICE_KEY
openai.api_base = AZURE_OPENAI_SERVICE_ENDPOINT # your endpoint should look like the following https://YOUR_RESOURCE_NAME.openai.azure.com/
openai.api_type = 'azure'
@@ -97,8 +100,8 @@ def get_chat_completions_async(system_prompt,user_prompt):
global response_content
try:
- response = ChatCompletion.create(model=AZURE_OPENAI_DEPLOYMENT_MODEL,deployment_id=AZURE_OPENAI_DEPLOYMENT_MODEL_NAME, messages=chat_request,max_tokens = 1000)
- except ex:
+ response = await ChatCompletion.acreate(model=AZURE_OPENAI_DEPLOYMENT_MODEL,deployment_id=AZURE_OPENAI_DEPLOYMENT_MODEL_NAME, messages=chat_request,max_tokens = 1000)
+ except Exception as ex:
app.logger.info("error in openai api call : %s",ex)
# Extract the response content
@@ -108,36 +111,41 @@ def get_chat_completions_async(system_prompt,user_prompt):
response_content=""
return response_content
-def get_chat_gpt_response(speech_input):
- return get_chat_completions_async(ANSWER_PROMPT_SYSTEM_TEMPLATE,speech_input)
+async def get_chat_gpt_response(speech_input):
+ return await get_chat_completions_async(ANSWER_PROMPT_SYSTEM_TEMPLATE,speech_input)
-def handle_recognize(replyText,callerId,call_connection_id,context=""):
- play_source = TextSource(text=replyText, voice_name="en-US-NancyNeural")
- recognize_result=call_automation_client.get_call_connection(call_connection_id).start_recognizing_media(
- input_type=RecognizeInputType.SPEECH,
- target_participant=PhoneNumberIdentifier(callerId),
- end_silence_timeout=10,
- play_prompt=play_source,
- operation_context=context)
- app.logger.info("handle_recognize : data=%s",recognize_result)
+async def handle_recognize(replyText,callerId,call_connection_id,context=""):
+ play_source = TextSource(text=replyText, voice_name="en-US-NancyNeural")
+ connection_client = call_automation_client.get_call_connection(call_connection_id)
+ try:
+ recognize_result = await connection_client.start_recognizing_media(
+ input_type=RecognizeInputType.SPEECH,
+ target_participant=PhoneNumberIdentifier(callerId),
+ end_silence_timeout=10,
+ play_prompt=play_source,
+ operation_context=context)
+ app.logger.info("handle_recognize : data=%s",recognize_result)
+ except Exception as ex:
+ app.logger.info("Error in recognize: %s", ex)
-def handle_play(call_connection_id, text_to_play, context):
+async def handle_play(call_connection_id, text_to_play, context):
play_source = TextSource(text=text_to_play, voice_name= "en-US-NancyNeural")
- call_automation_client.get_call_connection(call_connection_id).play_media_to_all(play_source,
- operation_context=context)
-
-def handle_hangup(call_connection_id):
- call_automation_client.get_call_connection(call_connection_id).hang_up(is_for_everyone=True)
+ await call_automation_client.get_call_connection(call_connection_id).play_media_to_all(
+ play_source,
+ operation_context=context)
+
+async def handle_hangup(call_connection_id):
+ await call_automation_client.get_call_connection(call_connection_id).hang_up(is_for_everyone=True)
-def detect_escalate_to_agent_intent(speech_text, logger):
- return has_intent_async(user_query=speech_text, intent_description="talk to agent", logger=logger)
+async def detect_escalate_to_agent_intent(speech_text, logger):
+ return await has_intent_async(user_query=speech_text, intent_description="talk to agent", logger=logger)
-def has_intent_async(user_query, intent_description, logger):
+async def has_intent_async(user_query, intent_description, logger):
is_match=False
system_prompt = "You are a helpful assistant"
combined_prompt = f"In 1 word: does {user_query} have a similar meaning as {intent_description}?"
#combined_prompt = base_user_prompt.format(user_query, intent_description)
- response = get_chat_completions_async(system_prompt, combined_prompt)
+ response = await get_chat_completions_async(system_prompt, combined_prompt)
if "yes" in response.lower():
is_match =True
logger.info(f"OpenAI results: is_match={is_match}, customer_query='{user_query}', intent_description='{intent_description}'")
@@ -149,9 +157,15 @@ def get_sentiment_score(sentiment_score):
match = regex.search(sentiment_score)
return int(match.group()) if match else -1
+async def answer_call_async(incoming_call_context,callback_url):
+ return await call_automation_client.answer_call(
+ incoming_call_context=incoming_call_context,
+ cognitive_services_endpoint=COGNITIVE_SERVICE_ENDPOINT,
+ callback_url=callback_url)
+
@app.route("/api/incomingCall", methods=['POST'])
-def incoming_call_handler():
- for event_dict in request.json:
+async def incoming_call_handler():
+ for event_dict in await request.json:
event = EventGridEvent.from_dict(event_dict)
app.logger.info("incoming event data --> %s", event.data)
if event.event_type == SystemEventNames.EventGridSubscriptionValidationEventName:
@@ -175,43 +189,41 @@ def incoming_call_handler():
app.logger.info("callback url: %s", callback_uri)
- answer_call_result = call_automation_client.answer_call(incoming_call_context=incoming_call_context,
- cognitive_services_endpoint=COGNITIVE_SERVICE_ENDPOINT,
- callback_url=callback_uri)
+ answer_call_result = await answer_call_async(incoming_call_context,callback_uri)
+
app.logger.info("Answered call for connection id: %s",
answer_call_result.call_connection_id)
return Response(status=200)
@app.route("/api/callbacks/", methods=["POST"])
-def handle_callback(contextId):
+async def handle_callback(contextId):
try:
- global caller_id , call_connection_id
- app.logger.info("Request Json: %s", request.json)
- for event_dict in request.json:
+ global caller_id
+ app.logger.info("Request Json: %s", await request.json)
+ for event_dict in await request.json:
event = CloudEvent.from_dict(event_dict)
- call_connection_id = event.data['callConnectionId']
-
- app.logger.info("%s event received for call connection id: %s", event.type, call_connection_id)
+ app.logger.info("%s event received for call connection id: %s", event.type, event.data['callConnectionId'])
caller_id = request.args.get("callerId").strip()
if "+" not in caller_id:
caller_id="+".strip()+caller_id.strip()
app.logger.info("call connected : data=%s", event.data)
if event.type == "Microsoft.Communication.CallConnected":
- handle_recognize(HELLO_PROMPT,
- caller_id,call_connection_id,
- context="GetFreeFormText")
+ await handle_recognize(HELLO_PROMPT,
+ caller_id, event.data['callConnectionId'],
+ context="GetFreeFormText")
elif event.type == "Microsoft.Communication.RecognizeCompleted":
if event.data['recognitionType'] == "speech":
speech_text = event.data['speechResult']['speech'];
app.logger.info("Recognition completed, speech_text =%s",
speech_text);
- if speech_text is not None and len(speech_text) > 0:
- if detect_escalate_to_agent_intent(speech_text=speech_text,logger=app.logger):
- handle_play(call_connection_id=call_connection_id,text_to_play=END_CALL_PHRASE_TO_CONNECT_AGENT,context=CONNECT_AGENT_CONTEXT)
- else:
- chat_gpt_response= get_chat_gpt_response(speech_text)
+ if speech_text is not None and len(speech_text) > 0:
+ detect_escalate = await detect_escalate_to_agent_intent(speech_text=speech_text,logger=app.logger)
+ if detect_escalate:
+ await handle_play(call_connection_id=event.data['callConnectionId'],text_to_play=END_CALL_PHRASE_TO_CONNECT_AGENT,context=CONNECT_AGENT_CONTEXT)
+ else:
+ chat_gpt_response = await get_chat_gpt_response(speech_text)
app.logger.info(f"Chat GPT response:{chat_gpt_response}")
regex = re.compile(CHAT_RESPONSE_EXTRACT_PATTERN)
match = regex.search(chat_gpt_response)
@@ -225,50 +237,49 @@ def handle_callback(contextId):
app.logger.info(f"Score={score}")
if -1 < score < 5:
app.logger.info(f"Score is less than 5")
- handle_play(call_connection_id=call_connection_id,text_to_play=CONNECT_AGENT_PROMPT,context=CONNECT_AGENT_CONTEXT)
+ await handle_play(call_connection_id=event.data['callConnectionId'],text_to_play=CONNECT_AGENT_PROMPT,context=CONNECT_AGENT_CONTEXT)
else:
app.logger.info(f"Score is more than 5")
- handle_recognize(answer,caller_id,call_connection_id,context="OpenAISample")
+ await handle_recognize(answer,caller_id,event.data['callConnectionId'],context="OpenAISample")
else:
app.logger.info("No match found")
- handle_recognize(chat_gpt_response,caller_id,call_connection_id,context="OpenAISample")
-
+ await handle_recognize(chat_gpt_response,caller_id,event.data['callConnectionId'],context="OpenAISample")
elif event.type == "Microsoft.Communication.RecognizeFailed":
resultInformation = event.data['resultInformation']
reasonCode = resultInformation['subCode']
context=event.data['operationContext']
global max_retry
if reasonCode == 8510 and 0 < max_retry:
- handle_recognize(TIMEOUT_SILENCE_PROMPT,caller_id,call_connection_id)
+ await handle_recognize(TIMEOUT_SILENCE_PROMPT,caller_id,event.data['callConnectionId'])
max_retry -= 1
else:
- handle_play(call_connection_id,GOODBYE_PROMPT, GOODBYE_CONTEXT)
+ await handle_play(event.data['callConnectionId'],GOODBYE_PROMPT, GOODBYE_CONTEXT)
elif event.type == "Microsoft.Communication.PlayCompleted":
context=event.data['operationContext']
- if context.lower() == TRANSFER_FAILED_CONTEXT.lower() or context.lower() == GOODBYE_CONTEXT.lower() :
- handle_hangup(call_connection_id)
+ if context.lower() == TRANSFER_FAILED_CONTEXT.lower() or context.lower() == GOODBYE_CONTEXT.lower():
+ await handle_hangup(event.data['callConnectionId'])
elif context.lower() == CONNECT_AGENT_CONTEXT.lower():
if not AGENT_PHONE_NUMBER or AGENT_PHONE_NUMBER.isspace():
app.logger.info(f"Agent phone number is empty")
- handle_play(call_connection_id=call_connection_id,text_to_play=AGENT_PHONE_NUMBER_EMPTY_PROMPT)
+ await handle_play(call_connection_id=event.data['callConnectionId'],text_to_play=AGENT_PHONE_NUMBER_EMPTY_PROMPT)
else:
app.logger.info(f"Initializing the Call transfer...")
transfer_destination=PhoneNumberIdentifier(AGENT_PHONE_NUMBER)
- call_connection_client =call_automation_client.get_call_connection(call_connection_id=call_connection_id)
- call_connection_client.transfer_call_to_participant(target_participant=transfer_destination)
+ call_connection_client = call_automation_client.get_call_connection(call_connection_id=event.data['callConnectionId'])
+ await call_connection_client.transfer_call_to_participant(target_participant=transfer_destination)
app.logger.info(f"Transfer call initiated: {context}")
elif event.type == "Microsoft.Communication.CallTransferAccepted":
- app.logger.info(f"Call transfer accepted event received for connection id: {call_connection_id}")
+ app.logger.info(f"Call transfer accepted event received for connection id: {event.data['callConnectionId']}")
elif event.type == "Microsoft.Communication.CallTransferFailed":
- app.logger.info(f"Call transfer failed event received for connection id: {call_connection_id}")
+ app.logger.info(f"Call transfer failed event received for connection id: {event.data['callConnectionId']}")
resultInformation = event.data['resultInformation']
sub_code = resultInformation['subCode']
# check for message extraction and code
- app.logger.info(f"Encountered error during call transfer, message=, code=, subCode={sub_code}")
- handle_play(call_connection_id=call_connection_id,text_to_play=CALLTRANSFER_FAILURE_PROMPT, context=TRANSFER_FAILED_CONTEXT)
+ app.logger.info(f"Encountered error during call transfer, message=, code=, subCode={sub_code}")
+ await handle_play(call_connection_id=event.data['callConnectionId'],text_to_play=CALLTRANSFER_FAILURE_PROMPT, context=TRANSFER_FAILED_CONTEXT)
return Response(status=200)
except Exception as ex:
app.logger.info("error in event handling")
diff --git a/callautomation-openai-sample/requirements.txt b/callautomation-openai-sample/requirements.txt
index 94dc155..fd40f70 100644
--- a/callautomation-openai-sample/requirements.txt
+++ b/callautomation-openai-sample/requirements.txt
@@ -1,4 +1,4 @@
-Flask>=2.3.2
+Quart>=0.19.6
azure-eventgrid==4.11.0
azure-communication-callautomation==1.1.0
openai==0.28.1
\ No newline at end of file
diff --git a/lobby-call-support-quickstart/Resources/Lobby_Call_Support_Scenario.jpg b/lobby-call-support-quickstart/Resources/Lobby_Call_Support_Scenario.jpg
new file mode 100644
index 0000000..0bfb895
Binary files /dev/null and b/lobby-call-support-quickstart/Resources/Lobby_Call_Support_Scenario.jpg differ
diff --git a/lobby-call-support-quickstart/main.py b/lobby-call-support-quickstart/main.py
new file mode 100644
index 0000000..c8a06b9
--- /dev/null
+++ b/lobby-call-support-quickstart/main.py
@@ -0,0 +1,436 @@
+import json
+import logging
+import os
+from typing import Any, Dict, List, Optional
+from urllib.parse import urljoin
+
+from azure.communication.callautomation import (
+ CommunicationUserIdentifier,
+ PhoneNumberIdentifier,
+ TextSource,
+)
+from azure.communication.callautomation.aio import CallAutomationClient
+from azure.core.messaging import CloudEvent
+from azure.eventgrid import EventGridEvent, SystemEventNames
+from fastapi import Body, FastAPI, HTTPException, WebSocket, WebSocketDisconnect
+from fastapi.responses import PlainTextResponse, Response
+from pydantic import BaseModel
+
+# Configuration constants
+ACS_CONNECTION_STRING = ""
+COGNITIVE_SERVICES_ENDPOINT = ""
+CALLBACK_URI_HOST = ""
+ACS_LOBBY_CALL_RECEIVER = ""
+ACS_TARGET_CALL_RECEIVER = ""
+
+# Default messages
+CONFIRM_MESSAGE_TO_TARGET_CALL = (
+ "A user is waiting in lobby, do you want to add the lobby user to your call?"
+)
+TEXT_TO_PLAY_TO_LOBBY_USER = (
+ "You are currently in a lobby call, we will notify the admin that you are waiting."
+)
+
+def validate_environment_variables() -> List[str]:
+ """Validate required environment variables and return missing ones."""
+ required_vars = [
+ ("ACS_CONNECTION_STRING", ACS_CONNECTION_STRING),
+ ("CALLBACK_URI_HOST", CALLBACK_URI_HOST),
+ ("COGNITIVE_SERVICES_ENDPOINT", COGNITIVE_SERVICES_ENDPOINT),
+ ("ACS_LOBBY_CALL_RECEIVER", ACS_LOBBY_CALL_RECEIVER),
+ ("ACS_TARGET_CALL_RECEIVER", ACS_TARGET_CALL_RECEIVER),
+ ]
+ return [var_name for var_name, var_value in required_vars if not var_value]
+
+
+def initialize_logging():
+ """Configure logging for the application."""
+ logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ )
+
+
+# Validate environment and initialize logging
+missing_vars = validate_environment_variables()
+if missing_vars:
+ print(f"Warning: Missing environment variables: {', '.join(missing_vars)}")
+ print("Use the /setConfigurations endpoint to set these values after startup.")
+
+initialize_logging()
+logger = logging.getLogger(__name__)
+
+# FastAPI application setup
+app = FastAPI(
+ title="Lobby Call Support Sample",
+ description="Azure Communication Services Call Automation Lobby Call Support Sample",
+ version="1.0.0",
+)
+
+# Application state
+class ApplicationState:
+ """Centralized application state management."""
+
+ def __init__(self):
+ # Configuration
+ self.acs_connection_string: str = ACS_CONNECTION_STRING
+ self.callback_uri_host: str = CALLBACK_URI_HOST
+ self.acs_lobby_call_receiver: str = ACS_LOBBY_CALL_RECEIVER
+ self.acs_target_call_receiver: str = ACS_TARGET_CALL_RECEIVER
+ self.confirm_message_to_target_call: str = CONFIRM_MESSAGE_TO_TARGET_CALL
+ self.text_to_play_to_lobby_user: str = TEXT_TO_PLAY_TO_LOBBY_USER
+
+ # Call tracking
+ self.target_call_connection_id: str = ""
+ self.lobby_connection_id: str = ""
+ self.lobby_caller_id: str = ""
+
+ # WebSocket connection
+ self.websocket_connection: Optional[WebSocket] = None
+
+
+app_state = ApplicationState()
+
+def initialize_call_automation_client(connection_string: str) -> Optional[CallAutomationClient]:
+ """Initialize CallAutomationClient with proper error handling."""
+ try:
+ if connection_string:
+ return CallAutomationClient.from_connection_string(connection_string)
+ logger.warning("CallAutomationClient not initialized - missing connection string")
+ return None
+ except Exception as e:
+ logger.error(f"Failed to initialize CallAutomationClient: {e}")
+ return None
+
+
+# Initialize CallAutomationClient
+client = initialize_call_automation_client(ACS_CONNECTION_STRING)
+
+
+async def get_and_log_participants(call_connection_id: str, context: str = "") -> Dict[str, Any]:
+ """
+ Get participants for a call connection and log the information.
+
+ Args:
+ call_connection_id: The call connection ID to get participants for
+ context: Optional context string for logging (e.g., "after move operation")
+
+ Returns:
+ Dictionary containing participant information and count
+
+ Raises:
+ Exception: If unable to retrieve participants
+ """
+ if not client:
+ raise Exception("Call automation client not initialized")
+
+ try:
+ call_connection = client.get_call_connection(call_connection_id)
+ participants_paged = call_connection.list_participants()
+
+ participants = []
+ async for participant in participants_paged:
+ identifier = participant.identifier
+
+ if isinstance(identifier, PhoneNumberIdentifier):
+ phone_num = getattr(identifier, 'phone_number', 'Unknown')
+ participant_info = {
+ "type": "PhoneNumberIdentifier",
+ "raw_id": identifier.raw_id,
+ "phone_number": phone_num
+ }
+ elif isinstance(identifier, CommunicationUserIdentifier):
+ participant_info = {
+ "type": "CommunicationUserIdentifier",
+ "raw_id": identifier.raw_id
+ }
+ else:
+ participant_info = {
+ "type": type(identifier).__name__,
+ "raw_id": identifier.raw_id
+ }
+
+ participants.append(participant_info)
+
+ participant_count = len(participants)
+ context_msg = f" {context}" if context else ""
+
+ logger.info(
+ f"Call {call_connection_id}{context_msg} has {participant_count} participant(s): "
+ f"{[p['raw_id'] for p in participants]}"
+ )
+
+ return {
+ "call_connection_id": call_connection_id,
+ "participant_count": participant_count,
+ "participants": participants
+ }
+
+ except Exception as e:
+ logger.error(f"Error getting participants for call {call_connection_id}: {e}")
+ raise
+
+# Pydantic models
+class ConfigurationRequest(BaseModel):
+ """Request model for updating application configuration."""
+
+ acs_connection_string: Optional[str] = None
+ callback_uri_host: Optional[str] = None
+ acs_lobby_call_receiver: Optional[str] = None
+ acs_target_call_receiver: Optional[str] = None
+ confirm_message_to_target_call: Optional[str] = None
+ text_to_play_to_lobby_user: Optional[str] = None
+
+
+class TargetCallRequest(BaseModel):
+ """Request model for creating a target call."""
+
+ acs_target: str
+# Event Handler
+@app.post("/api/LobbyCallSupportEventHandler")
+async def lobby_call_support_event_handler(events: List[Dict[str, Any]] = Body(...)):
+ """Handle Event Grid events for lobby call support scenario."""
+ if not client:
+ raise HTTPException(status_code=500, detail="Call automation client not initialized")
+
+ try:
+ for event_data in events:
+ event = EventGridEvent.from_dict(event_data)
+
+ if event.event_type == SystemEventNames.EventGridSubscriptionValidationEventName:
+ validation_code = event.data.get("validationCode", "")
+ return Response(
+ content=json.dumps({"validationResponse": validation_code}),
+ status_code=200,
+ media_type="application/json",
+ )
+
+ elif event.event_type == "Microsoft.Communication.IncomingCall":
+ incoming_call_data = event.data
+ logger.info(f"Event received: {event.event_type}")
+
+ from_caller_id = (
+ event.data['from']["phoneNumber"]["value"]
+ if event.data['from']['kind'] == "phoneNumber"
+ else event.data['from']['rawId']
+ )
+ to_caller_id = (
+ event.data['to']["phoneNumber"]["value"]
+ if event.data['to']['kind'] == "phoneNumber"
+ else event.data['to']['rawId']
+ )
+
+ # Check if this is a lobby or target call
+ if (app_state.acs_lobby_call_receiver in to_caller_id or
+ app_state.acs_target_call_receiver in to_caller_id):
+
+ callback_uri = urljoin(app_state.callback_uri_host, "/api/callbacks")
+ operation_context = (
+ "LobbyCall"
+ if app_state.acs_target_call_receiver not in to_caller_id
+ else "OtherCall"
+ )
+
+ answer_call_result = await client.answer_call(
+ incoming_call_context=incoming_call_data.get("incomingCallContext"),
+ callback_url=callback_uri,
+ operation_context=operation_context,
+ cognitive_services_endpoint=COGNITIVE_SERVICES_ENDPOINT,
+ )
+
+ if app_state.acs_target_call_receiver in to_caller_id:
+ app_state.target_call_connection_id = answer_call_result.call_connection_id
+ logger.info(f"Target call answered: {app_state.target_call_connection_id}")
+ else:
+ app_state.lobby_connection_id = answer_call_result.call_connection_id
+ logger.info(f"Lobby call answered: {app_state.lobby_connection_id}")
+
+ return PlainTextResponse(content="Events processed successfully")
+
+ except Exception as e:
+ logger.error(f"Error processing lobby call support event: {e}")
+ return Response(content=str(e), status_code=500)
+
+@app.post("/api/callbacks")
+async def callbacks(events: List[Dict[str, Any]] = Body(...)):
+ """Handle Call Automation callback events."""
+ if not client:
+ raise HTTPException(status_code=500, detail="Call automation client not initialized")
+
+ try:
+ for event_data in events:
+ cloud_event = CloudEvent.from_dict(event_data)
+ operation_context = cloud_event.data.get('operationContext', '')
+ event_type = cloud_event.data.get('type', cloud_event.type)
+ call_connection_id = (
+ cloud_event.data.get('callConnectionId') or
+ cloud_event.data["callConnectionId"]
+ )
+
+ logger.info(f"Received callback event: {event_type}, Context: {operation_context}")
+
+ # Handle CallConnected event
+ if "CallConnected" in event_type:
+ if operation_context == "LobbyCall":
+ # Get lobby caller information
+ lobby_call_connection = client.get_call_connection(call_connection_id)
+ call_properties = await lobby_call_connection.get_call_properties()
+ app_state.lobby_caller_id = call_properties.source.raw_id
+ app_state.lobby_connection_id = call_properties.call_connection_id
+
+ # Play message to lobby user
+ text_source = TextSource(
+ text=app_state.text_to_play_to_lobby_user,
+ voice_name="en-US-NancyNeural"
+ )
+ await lobby_call_connection.play_media(
+ play_source=[text_source],
+ play_to=[CommunicationUserIdentifier(app_state.lobby_caller_id)]
+ )
+
+ # Handle PlayCompleted event
+ elif "PlayCompleted" in event_type:
+ # Notify target call user via WebSocket
+ if app_state.websocket_connection is None:
+ logger.warning("WebSocket connection not available")
+ return Response(content="WebSocket not available", status_code=404)
+
+ await app_state.websocket_connection.send_text(
+ app_state.confirm_message_to_target_call
+ )
+ logger.info("Target call user notified via WebSocket")
+ return Response(content="Target call user notified")
+
+ # Handle MoveParticipantSucceeded event
+ elif "MoveParticipantSucceeded" in event_type:
+ logger.info(f"Participant moved successfully: {call_connection_id}")
+
+ # Log participants count after successful move operation
+ try:
+ await get_and_log_participants(call_connection_id, "after move operation")
+ except Exception as e:
+ logger.error(f"Failed to get participants after move operation: {e}")
+
+ # Handle CallDisconnected event
+ elif "CallDisconnected" in event_type:
+ logger.info(f"Call disconnected: {call_connection_id}")
+
+ return PlainTextResponse(content="Callbacks processed successfully")
+
+ except Exception as e:
+ logger.error(f"Error processing callback: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/TargetCallToAcsUser", tags=["Lobby Call Support APIs"])
+async def target_call_to_acs_user(request: TargetCallRequest):
+ """Create Target Call to ACS User."""
+ if not client:
+ raise HTTPException(status_code=500, detail="Call automation client not initialized")
+
+ try:
+ callback_uri = urljoin(app_state.callback_uri_host, "/api/callbacks")
+
+ create_call_result = await client.create_call(
+ target_participant=CommunicationUserIdentifier(request.acs_target),
+ callback_url=callback_uri
+ )
+
+ app_state.target_call_connection_id = create_call_result.call_connection_id
+
+ logger.info(f"Target call created: {app_state.target_call_connection_id}")
+ return {
+ "message": "Target call created successfully",
+ "call_connection_id": app_state.target_call_connection_id,
+ "correlation_id": create_call_result.correlation_id
+ }
+
+ except Exception as e:
+ logger.error(f"Error creating target call: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.get("/GetParticipants/{call_connection_id}", tags=["Lobby Call Support APIs"])
+async def get_participants(call_connection_id: str):
+ """Get participants for a specific call connection."""
+ try:
+ result = await get_and_log_participants(call_connection_id, "via API request")
+
+ if result["participant_count"] == 0:
+ raise HTTPException(
+ status_code=404,
+ detail="No participants found for the specified call connection"
+ )
+
+ return result
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error in get_participants API for call {call_connection_id}: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.websocket("/ws")
+async def websocket_endpoint(websocket: WebSocket):
+ """WebSocket endpoint for real-time communication with client app."""
+ if not client:
+ await websocket.close(code=1000, reason="Call automation client not initialized")
+ return
+
+ await websocket.accept()
+ app_state.websocket_connection = websocket
+ logger.info("WebSocket connection established")
+
+ try:
+ while True:
+ # Receive message from client
+ data = await websocket.receive_text()
+ logger.info(f"Received WebSocket message: {data}")
+
+ # Process incoming message
+ if data.lower() == "yes":
+ await _handle_move_participant()
+
+ except WebSocketDisconnect:
+ logger.info("WebSocket disconnected")
+ app_state.websocket_connection = None
+ except Exception as e:
+ logger.error(f"WebSocket error: {e}")
+ app_state.websocket_connection = None
+
+
+async def _handle_move_participant():
+ """Handle moving participant from lobby to target call."""
+ try:
+ if not all([app_state.lobby_caller_id, app_state.lobby_connection_id,
+ app_state.target_call_connection_id]):
+ logger.error("Missing required call information for move operation")
+ return
+
+ logger.info(f"Moving participant {app_state.lobby_caller_id} from lobby to target call")
+
+ # Get the target connection and move the participant
+ target_connection = client.get_call_connection(app_state.target_call_connection_id)
+ response = await target_connection.move_participants(
+ target_participants=[CommunicationUserIdentifier(app_state.lobby_caller_id)],
+ from_call=app_state.lobby_connection_id
+ )
+
+ logger.info("Move Participants operation is initiated.")
+
+ except Exception as e:
+ logger.error(f"Error in move participants operation: {e}")
+
+def main():
+ """Main entry point for the application."""
+ import uvicorn
+
+ uvicorn.run(
+ app,
+ host="0.0.0.0",
+ port=8080,
+ log_level="info",
+ access_log=True,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/lobby-call-support-quickstart/readme.md b/lobby-call-support-quickstart/readme.md
new file mode 100644
index 0000000..de7197d
--- /dev/null
+++ b/lobby-call-support-quickstart/readme.md
@@ -0,0 +1,189 @@
+| page_type | languages | products |
+| --------- | --------------------------------------- | --------------------------------------------------------------------------- |
+| sample | | | azure | azure-communication-services |
|
+
+# Call Automation – Lobby Call Support Sample
+
+This sample demonstrates how to use the Call Automation SDK to implement a Lobby Call scenario with Azure Communication Services. Users join a lobby call and remain on hold until a participant in the target call confirms their participation. Once approved, the Call Automation bot automatically connects the lobby user to the designated target call.
+
+---
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Design](#design)
+- [Prerequisites](#prerequisites)
+- [Getting Started](#getting-started)
+- [Configuration](#configuration)
+- [Running the App Locally](#running-the-app-locally)
+- [Workflow](#workflow)
+- [Troubleshooting](#troubleshooting)
+
+---
+
+## Overview
+
+This project provides a sample implementation for lobby call handling using Azure Communication Services and the Call Automation SDK.
+
+---
+
+## Design
+
+
+
+---
+
+## Prerequisites
+
+- **Azure Account:** An Azure account with an active subscription.
+ https://azure.microsoft.com/free/?WT.mc_id=A261C142F.
+- **Communication Services Resource:** A deployed Communication Services resource.
+ https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource.
+- **Phone Number:** A https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/telephony/get-phone-number in your ACS resource that can make outbound calls.
+- **Azure AI Multi-Service Resource:**
+ https://learn.microsoft.com/en-us/azure/cognitive-services/cognitive-services-apis-create-account.
+- **Azure Dev Tunnel:**
+ https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started.
+- **Client Application:**
+ Navigate to `LobbyCallSupport-Client` folder in https://github.com/Azure-Samples/communication-services-javascript-quickstarts.
+
+---
+
+## Getting Started
+
+### Clone the Source Code
+
+1. Open PowerShell, Windows Terminal, Command Prompt, or equivalent.
+2. Navigate to your desired directory.
+3. Clone the repository:
+ ```sh
+ git clone https://github.com/Azure-Samples/communication-services-python-quickstarts.git
+ ```
+
+### Set Up Python Virtual Environment and install Dependencies
+
+```bash
+cd communication-services-python-quickstarts/lobby-call-support-quickstart
+python -m venv venv
+venv\Scripts\activate
+pip install -r requirements.txt
+```
+
+## Setup and Host Azure Dev Tunnel
+
+```
+devtunnel create --allow-anonymous
+devtunnel port create -p 8080
+devtunnel host
+
+```
+
+---
+
+## Configuration
+
+Before running the application, initialize the following constants in the `main.py` file.
+
+| Setting | Description | Example Value |
+| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
+| `ACS_CONNECTION_STRING` | The connection string for your Azure Communication Services resource. Find this in the Azure Portal under your resource’s **Keys** section. | `"endpoint=https://.communication.azure.com/;accesskey="` |
+| `COGNITIVE_SERVICES_ENDPOINT` | The endpoint for your Azure Cognitive Services resource. Used to play media to participants in the call. | `"https://"` |
+| `CALLBACK_URI_HOST` | The base URL where your app will listen for incoming events from Azure Communication Services. For local development, use your Azure Dev Tunnel URL. | `"https://.devtunnels.ms"` |
+| `ACS_LOBBY_CALL_RECEIVER` | ACS identity for the lobby call receiver. Generated using ACS SDK or Azure Portal. | `"8:acs:"` |
+| `ACS_TARGET_CALL_RECEIVER` | ACS identity for the target call receiver. Generated using ACS SDK or Azure Portal. | `"8:acs:"` |
+
+---
+
+### How to Obtain These Values
+
+- **ACS_CONNECTION_STRING:**
+
+ 1. Go to the Azure Portal.
+ 2. Navigate to your Communication Services resource.
+ 3. Select **Keys & Connection String**.
+ 4. Copy the **Connection String** value.
+
+- **COGNITIVE_SERVICES_ENDPOINT:**
+
+ 1. Create an Azure AI Multi-Service resource.
+ 2. Copy the endpoint from the resource overview page.
+
+- **CALLBACK_URI_HOST:**
+
+ 1. Set up an Azure Dev Tunnel as described in the prerequisites.
+ 2. Use the public URL provided by the Dev Tunnel as your callback URI host.
+
+- **ACS_LOBBY_CALL_RECEIVER / ACS_TARGET_CALL_RECEIVER:**
+ 1. Use the ACS web client or SDK to generate user identities.
+ 2. Store the generated identity strings here.
+
+#### Example `config variables`
+
+```
+ ACS_CONNECTION_STRING = "endpoint=https://.communication.azure.com/;accesskey=",
+ COGNITIVE_SERVICE_ENDPOINT: "https://",
+ CALLBACK_URI_HOST = "https://.devtunnels.ms",
+ ACS_LOBBY_CALL_RECEIVER = "+1XXXXXXXXXX",
+ ACS_TARGET_CALL_RECEIVER = "+1XXXXXXXXXX"
+
+```
+
+---
+
+## Running the App Locally
+
+1. **Generate ACS identities** for lobby and target participants in **Azure Portal**. 3 users are needed:
+ - `acsLobbyCallReceiver` – Lobby call receiver.
+ - `acsTargetCallReceiver` – Target call receiver.
+ - `Sender` – Target call sender.
+2. **Setup EventSubscription** for incoming calls:
+ - Set up a Web hook(`https:///api/LobbyCallSupportEventHandler`) for callback.
+ - Add Filter:
+ - Key: `data.to.rawid`, operator: `string contains`, value: `acsLobbyCallReceiver, acsTargetCallReceiver`
+3. Use the **JS Client App**, Navigate to `LobbyCallSupport-Client` folder in https://github.com/Azure-Samples/communication-services-javascript-quickstarts.
+4. Use the **WebSocket**, `wss:///ws` in client app for client-server communication.
+
+---
+
+## Workflow
+
+- Start target call in client app `LobbyCallSupport-Client`:
+ - Input token for `Sender` behalf of Target call sender.
+ - Input user ID for `acsTargetCallReceiver` for Target call receiver.
+ - Click **Start Call**.
+- Incoming call from target sender → server answers → expect `Call Connected` event.
+- From a test app or a client app, **Lobby user** calls `acsLobbyCallReceiver` → CA answers call and automated voice plays: `You are currently in a lobby call, we will notify the admin that you are waiting.`
+- Target call receives notification (a confirm dialog): `A user is waiting in lobby, do you want to add them to your call?`
+- If confirmed, **Lobby user must accept the call when prompted to move in call test app** → expect **MoveParticipantSucceeded** event → lobby user joins target call.
+- **If user does not accept the move call prompt → lobby user remains in lobby call.**
+- If Target user declined → lobby user will not be moved to target call.
+
+---
+
+## API Testing with Swagger
+
+You can explore and test the available API endpoints using the built-in Swagger UI:
+
+- **Swagger URL:**
+ [https://localhost:8080/docs](https://localhost:8080/docs)
+
+- > If running in a dev tunnel or cloud environment, replace `localhost:8080` with your tunnel's public URL (e.g., `https://.devtunnels.ms/docs`).
+
+---
+
+## Troubleshooting
+
+### Common Issues
+
+- **Invalid ACS Connection String:**
+ Verify `ACS_CONNECTION_STRING` in `config variables`.
+- **Callback URL Not Reachable:**
+ Ensure Dev Tunnel is running and URL matches `CALLBACK_URI_HOST`.
+- **Identity Errors:**
+ Regenerate ACS identities if invalid.
+
+**Still having trouble?**
+
+- Review the official https://learn.microsoft.com/azure/communication-services/.
+- Search for similar issues or ask questions on https://learn.microsoft.com/answers/topics/azure-communication-services.html.
+- Contact your Azure administrator or support team if you suspect a permissions or resource issue.
diff --git a/lobby-call-support-quickstart/requirements.txt b/lobby-call-support-quickstart/requirements.txt
new file mode 100644
index 0000000..44abb3a
--- /dev/null
+++ b/lobby-call-support-quickstart/requirements.txt
@@ -0,0 +1,7 @@
+fastapi>=0.110.0
+uvicorn[standard]>=0.27.0
+azure-eventgrid==4.11.0
+azure-communication-callautomation==1.6.0-beta.1
+aiohttp>=3.11.18
+jinja2>=3.1.2
+pydantic>=2.0
diff --git a/lookup-phone-numbers-quickstart/README.md b/lookup-phone-numbers-quickstart/README.md
index 7f17657..e2d30ac 100644
--- a/lookup-phone-numbers-quickstart/README.md
+++ b/lookup-phone-numbers-quickstart/README.md
@@ -24,7 +24,7 @@ For full instructions on how to build this code sample from scratch, look at [Qu
## Install the packages
-pip install azure-communication-phonenumbers==1.2.0b2
+pip install azure-communication-phonenumbers==1.2.0
pip install azure-identity
diff --git a/lookup-phone-numbers-quickstart/number-lookup-sample.py b/lookup-phone-numbers-quickstart/number-lookup-sample.py
index 8d594a2..5319904 100644
--- a/lookup-phone-numbers-quickstart/number-lookup-sample.py
+++ b/lookup-phone-numbers-quickstart/number-lookup-sample.py
@@ -25,7 +25,7 @@
# Use the paid number lookup functionality to get operator specific details
# IMPORTANT NOTE: Invoking the method below will incur a charge to your account
options = { "include_additional_operator_details": True }
- operator_results = phone_numbers_client.search_operator_information([ phoneNumber ], options)
+ operator_results = phone_numbers_client.search_operator_information([ phoneNumber ], options=options)
operator_information = operator_results.values[0]
number_type = operator_information.number_type if operator_information.number_type else "unknown"
diff --git a/send-email-advanced/README.md b/send-email-advanced/README.md
index 2488552..a15eb8b 100644
--- a/send-email-advanced/README.md
+++ b/send-email-advanced/README.md
@@ -39,6 +39,10 @@ The advanced version of send-email includes the following sub samples.
- ./send-email-advanced/send-email-attachments/send-email-attachments.py
+### Send email with inline attachments
+
+- ./send-email-advanced/send-email-inline-attachments/send-email-inline.attachments.py
+
### Send email to multiple recipients
- ./send-email-advanced/send-email-multiple-recipients/send-email-multiple-recipients.py
diff --git a/send-email-advanced/send-email-inline-attachments/inline-attachment.jpg b/send-email-advanced/send-email-inline-attachments/inline-attachment.jpg
new file mode 100644
index 0000000..97fc2f8
Binary files /dev/null and b/send-email-advanced/send-email-inline-attachments/inline-attachment.jpg differ
diff --git a/send-email-advanced/send-email-inline-attachments/inline-attachment.png b/send-email-advanced/send-email-inline-attachments/inline-attachment.png
new file mode 100644
index 0000000..715945b
Binary files /dev/null and b/send-email-advanced/send-email-inline-attachments/inline-attachment.png differ
diff --git a/send-email-advanced/send-email-inline-attachments/send-email-inline-attachments.py b/send-email-advanced/send-email-inline-attachments/send-email-inline-attachments.py
new file mode 100644
index 0000000..79fd0db
--- /dev/null
+++ b/send-email-advanced/send-email-inline-attachments/send-email-inline-attachments.py
@@ -0,0 +1,63 @@
+import base64
+from azure.communication.email import EmailClient
+
+with open("./inline-attachment.jpg", "rb") as file:
+ jpg_b64encoded = base64.b64encode(file.read())
+
+with open("./inline-attachment.png", "rb") as file:
+ png_b64encoded = base64.b64encode(file.read())
+
+connection_string = ""
+sender_address = ""
+recipient_address = ""
+
+POLLER_WAIT_TIME = 10
+
+message = {
+ "senderAddress": sender_address,
+ "recipients": {
+ "to": [{ "address": recipient_address }]
+ },
+ "content": {
+ "subject": "Test email from Python Sample",
+ "plainText": "This is plaintext body of test email.",
+ "html": "HTML body inline images:

"
+ },
+ "attachments": [
+ {
+ "name": "inline-attachments.jpg",
+ "contentId": "kittens-1",
+ "contentType": "image/jpeg",
+ "contentInBase64": jpg_b64encoded.decode()
+ },
+ {
+ "name": "inline-attachments.png",
+ "contentId": "kittens-2",
+ "contentType": "image/png",
+ "contentInBase64": png_b64encoded.decode()
+ }
+ ]
+}
+
+try:
+ client = EmailClient.from_connection_string(connection_string)
+
+ poller = client.begin_send(message);
+
+ time_elapsed = 0
+ while not poller.done():
+ print("Email send poller status: " + poller.status())
+
+ poller.wait(POLLER_WAIT_TIME)
+ time_elapsed += POLLER_WAIT_TIME
+
+ if time_elapsed > 18 * POLLER_WAIT_TIME:
+ raise RuntimeError("Polling timed out.")
+
+ if poller.result()["status"] == "Succeeded":
+ print(f"Successfully sent the email (operation id: {poller.result()['id']})")
+ else:
+ raise RuntimeError(str(poller.result()["error"]))
+
+except Exception as ex:
+ print(ex)