Skip to content

Commit

Permalink
quality: Document call events
Browse files Browse the repository at this point in the history
  • Loading branch information
clemlesne committed Dec 8, 2024
1 parent 5e829e0 commit d5c80a4
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 2 deletions.
60 changes: 59 additions & 1 deletion app/helpers/call_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ async def on_new_call(
phone_number: str,
wss_url: str,
) -> bool:
"""
Callback for when a new call is received.
Answers the call and starts the media streaming.
"""
logger.debug("Incoming call handler")

streaming_options = MediaStreamingOptions(
Expand Down Expand Up @@ -99,6 +104,11 @@ async def on_call_connected(
client: CallAutomationClient,
server_call_id: str,
) -> None:
"""
Callback for when the call is connected.
Ask for the language and start recording the call.
"""
logger.info("Call connected, asking for language")

# Add define the call as in progress
Expand Down Expand Up @@ -133,6 +143,11 @@ async def on_call_disconnected(
client: CallAutomationClient,
post_callback: Callable[[CallStateModel], Awaitable[None]],
) -> None:
"""
Callback for when the call is disconnected.
Hangs up the call and stores the final message.
"""
logger.info("Call disconnected")
await _handle_hangup(
call=call,
Expand All @@ -152,6 +167,9 @@ async def on_audio_connected( # noqa: PLR0913
post_callback: Callable[[CallStateModel], Awaitable[None]],
training_callback: Callable[[CallStateModel], Awaitable[None]],
) -> None:
"""
Callback for when the audio stream is connected.
"""
await load_llm_chat(
audio_bits_per_sample=audio_bits_per_sample,
audio_channels=audio_channels,
Expand Down Expand Up @@ -248,6 +266,11 @@ async def _handle_goodbye(
async def on_play_started(
call: CallStateModel,
) -> None:
"""
Callback for when a media play action starts.
Updates the last interaction time.
"""
logger.debug("Play started")

# Enrich span
Expand All @@ -265,6 +288,11 @@ async def on_play_completed(
contexts: set[CallContextEnum] | None,
post_callback: Callable[[CallStateModel], Awaitable[None]],
) -> None:
"""
Callback for when a media play action completes.
If the call should continue, updates the last interaction time. Else, hangs up the call.
"""
logger.debug("Play completed")

# Enrich span
Expand Down Expand Up @@ -306,6 +334,11 @@ async def on_play_completed(

@tracer.start_as_current_span("on_play_error")
async def on_play_error(error_code: int) -> None:
"""
Callback for when a media play action fails.
Logs the error and suppresses known errors from the Communication Services SDK.
"""
logger.debug("Play failed")

# Enrich span
Expand Down Expand Up @@ -338,6 +371,9 @@ async def on_ivr_recognized(
client: CallAutomationClient,
label: str,
) -> None:
"""
Callback for when an IVR recognition is successful.
"""
logger.info("IVR recognized: %s", label)

# Enrich span
Expand Down Expand Up @@ -399,6 +435,11 @@ async def on_transfer_error(
client: CallAutomationClient,
error_code: int,
) -> None:
"""
Callback for when a call transfer fails.
Logs the error and plays a TTS message to the user.
"""
logger.info("Error during call transfer, subCode %s", error_code)
await handle_play_text(
call=call,
Expand All @@ -413,6 +454,11 @@ async def on_sms_received(
call: CallStateModel,
message: str,
) -> bool:
"""
Callback for when an SMS is received.
Adds the SMS to the call history and answers with voice if the call is in progress. If not, answers with SMS.
"""
logger.info("SMS received from %s: %s", call.initiate.phone_number, message)

# Enrich span
Expand Down Expand Up @@ -470,7 +516,9 @@ async def on_end_call(
call: CallStateModel,
) -> None:
"""
Shortcut to run all post-call intelligence tasks in background.
Callback for when a call ends.
Generates post-call intelligence if the call had interactions.
"""
if (
len(call.messages) >= 3 # noqa: PLR2004
Expand Down Expand Up @@ -603,6 +651,11 @@ async def _handle_ivr_language(
call: CallStateModel,
client: CallAutomationClient,
) -> None:
"""
Handle IVR language selection.
If only one language is available, selects it by default. Else, plays the IVR prompt.
"""
# If only one language is available, skip the IVR
if len(CONFIG.conversation.initiate.lang.availables) == 1:
short_code = CONFIG.conversation.initiate.lang.availables[0].short_code
Expand Down Expand Up @@ -648,6 +701,11 @@ async def _handle_recording(
client: CallAutomationClient,
server_call_id: str,
) -> None:
"""
Start recording the call.
Feature activation is checked before starting the recording.
"""
if not await recording_enabled():
return

Expand Down
28 changes: 27 additions & 1 deletion app/helpers/call_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ async def _chunk_before_tts(
) -> list[str]:
"""
Split a text in chunks and store them in the call messages.
Chunks are separated by sentences and are limited to the TTS capacity.
"""
# Sanitize text for TTS
text = re.sub(_TTS_SANITIZER_R, " ", text) # Remove unwanted characters
Expand Down Expand Up @@ -266,7 +268,7 @@ def _audio_from_text(
"""
Generate an audio source that can be read by Azure Communication Services SDK.
Text requires to be SVG escaped, and SSML tags are used to control the voice. Plus, text is slowed down by 5% to make it more understandable for elderly people. Text is also truncated, as this is the limit of Azure Communication Services TTS, but a warning is logged.
Text requires to be SVG escaped, and SSML tags are used to control the voice. Text is also truncated, as this is the limit of Azure Communication Services TTS, but a warning is logged.
See: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-structure
"""
Expand Down Expand Up @@ -331,6 +333,11 @@ async def handle_hangup(
client: CallAutomationClient,
call: CallStateModel,
) -> None:
"""
Hang up a call.
If the call is already hung up, the exception will be suppressed.
"""
logger.info("Hanging up: %s", call.initiate.phone_number)
with (
# Suppress hangup exception
Expand All @@ -349,6 +356,11 @@ async def handle_transfer(
target: str,
context: ContextEnum | None = None,
) -> None:
"""
Transfer a call to another participant.
Can raise a `CallHangupException` if the call is hung up.
"""
logger.info("Transferring call: %s", target)
with _detect_hangup():
assert call.voice_id, "Voice ID is required to control the call"
Expand All @@ -363,6 +375,11 @@ async def start_audio_streaming(
client: CallAutomationClient,
call: CallStateModel,
) -> None:
"""
Start audio streaming to the call.
Can raise a `CallHangupException` if the call is hung up.
"""
logger.info("Starting audio streaming")
with _detect_hangup():
assert call.voice_id, "Voice ID is required to control the call"
Expand All @@ -379,6 +396,11 @@ async def stop_audio_streaming(
client: CallAutomationClient,
call: CallStateModel,
) -> None:
"""
Stop audio streaming to the call.
Can raise a `CallHangupException` if the call is hung up.
"""
logger.info("Stopping audio streaming")
with _detect_hangup():
assert call.voice_id, "Voice ID is required to control the call"
Expand Down Expand Up @@ -419,4 +441,8 @@ def _detect_hangup() -> Generator[None, None, None]:
async def _use_call_client(
client: CallAutomationClient, voice_id: str
) -> AsyncGenerator[CallConnectionClient, None]:
"""
Return the call client for a given call.
"""
# Client already been created in the call client, never close it from here
yield client.get_call_connection(call_connection_id=voice_id)

0 comments on commit d5c80a4

Please sign in to comment.