mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-25 08:48:13 +02:00
feat: tansfer calls with aasterisk (#171)
* feat: tansfer calls with aasterisk * chore: format code with pre-commit script * chore: refactor code * refactor: add call strategies, cleanup transfer events * fix: docker compose, add missing files from merge conflicts * chore: update pipecat * docs: restructure & add mintilify pages for tool * chore: upgrade pipecat
This commit is contained in:
parent
9e058699c5
commit
bd07b753cd
19 changed files with 1043 additions and 106 deletions
|
|
@ -318,7 +318,6 @@ class CustomToolManager:
|
|||
"message": "I'm sorry, but call transfers are not available for web calls. Please try a telephony call.",
|
||||
"action": "transfer_failed",
|
||||
"reason": "webrtc_not_supported",
|
||||
"end_call": True,
|
||||
}
|
||||
await self._handle_transfer_result(
|
||||
webrtc_error_result, function_call_params, properties
|
||||
|
|
@ -332,27 +331,46 @@ class CustomToolManager:
|
|||
"message": "I'm sorry, but I don't have a phone number configured for the transfer. Please contact support to set up call transfer.",
|
||||
"action": "transfer_failed",
|
||||
"reason": "no_destination",
|
||||
"end_call": True,
|
||||
}
|
||||
await self._handle_transfer_result(
|
||||
validation_error_result, function_call_params, properties
|
||||
)
|
||||
return
|
||||
|
||||
# Validate E.164 format
|
||||
E164_PHONE_REGEX = r"^\+[1-9]\d{1,14}$"
|
||||
if not re.match(E164_PHONE_REGEX, destination):
|
||||
validation_error_result = {
|
||||
"status": "failed",
|
||||
"message": "I'm sorry, but the transfer phone number appears to be invalid. Please contact support to verify the transfer settings.",
|
||||
"action": "transfer_failed",
|
||||
"reason": "invalid_destination",
|
||||
"end_call": True,
|
||||
}
|
||||
await self._handle_transfer_result(
|
||||
validation_error_result, function_call_params, properties
|
||||
)
|
||||
return
|
||||
# Validate destination format based on workflow run mode
|
||||
if workflow_run.mode == WorkflowRunMode.ARI.value:
|
||||
# For ARI provider, also accept SIP endpoints
|
||||
SIP_ENDPOINT_REGEX = r"^(PJSIP|SIP)\/[\w\-\.@]+$"
|
||||
E164_PHONE_REGEX = r"^\+[1-9]\d{1,14}$"
|
||||
|
||||
is_valid_sip = re.match(SIP_ENDPOINT_REGEX, destination)
|
||||
is_valid_e164 = re.match(E164_PHONE_REGEX, destination)
|
||||
|
||||
if not (is_valid_sip or is_valid_e164):
|
||||
validation_error_result = {
|
||||
"status": "failed",
|
||||
"message": "I'm sorry, but the transfer destination appears to be invalid. Please contact support to verify the transfer settings.",
|
||||
"action": "transfer_failed",
|
||||
"reason": "invalid_destination",
|
||||
}
|
||||
await self._handle_transfer_result(
|
||||
validation_error_result, function_call_params, properties
|
||||
)
|
||||
return
|
||||
else:
|
||||
# For non-ARI providers (Twilio, etc), use E.164 validation
|
||||
E164_PHONE_REGEX = r"^\+[1-9]\d{1,14}$"
|
||||
if not re.match(E164_PHONE_REGEX, destination):
|
||||
validation_error_result = {
|
||||
"status": "failed",
|
||||
"message": "I'm sorry, but the transfer phone number appears to be invalid. Please contact support to verify the transfer settings.",
|
||||
"action": "transfer_failed",
|
||||
"reason": "invalid_destination",
|
||||
}
|
||||
await self._handle_transfer_result(
|
||||
validation_error_result, function_call_params, properties
|
||||
)
|
||||
return
|
||||
|
||||
if message_type == "custom" and custom_message:
|
||||
logger.info(f"Playing pre-transfer message: {custom_message}")
|
||||
|
|
@ -366,14 +384,12 @@ class CustomToolManager:
|
|||
"message": "I'm sorry, there's an issue with this call transfer. Please contact support.",
|
||||
"action": "transfer_failed",
|
||||
"reason": "no_organization_id",
|
||||
"end_call": False,
|
||||
}
|
||||
await self._handle_transfer_result(
|
||||
validation_error_result, function_call_params, properties
|
||||
)
|
||||
return
|
||||
|
||||
# Get telephony provider directly (no HTTP round-trip)
|
||||
provider = await get_telephony_provider(organization_id)
|
||||
if not provider.supports_transfers() or not provider.validate_config():
|
||||
validation_error_result = {
|
||||
|
|
@ -381,7 +397,6 @@ class CustomToolManager:
|
|||
"message": "I'm sorry, there's an issue with this call transfer. Please contact support.",
|
||||
"action": "transfer_failed",
|
||||
"reason": "provider_does_not_support_transfer",
|
||||
"end_call": False,
|
||||
}
|
||||
await self._handle_transfer_result(
|
||||
validation_error_result, function_call_params, properties
|
||||
|
|
@ -396,6 +411,19 @@ class CustomToolManager:
|
|||
# Compute conference name from original call SID
|
||||
conference_name = f"transfer-{original_call_sid}"
|
||||
|
||||
# Store initial transfer context in Redis before provider call to avoid race condition
|
||||
call_transfer_manager = await get_call_transfer_manager()
|
||||
transfer_context = TransferContext(
|
||||
transfer_id=transfer_id,
|
||||
call_sid=None, # Will be updated after provider response
|
||||
target_number=destination,
|
||||
tool_uuid=tool.tool_uuid,
|
||||
original_call_sid=original_call_sid,
|
||||
conference_name=conference_name,
|
||||
initiated_at=time.time(),
|
||||
)
|
||||
await call_transfer_manager.store_transfer_context(transfer_context)
|
||||
|
||||
# Mute the pipeline
|
||||
self._engine.set_mute_pipeline(True)
|
||||
|
||||
|
|
@ -410,21 +438,8 @@ class CustomToolManager:
|
|||
call_sid = transfer_result.get("call_sid")
|
||||
logger.info(f"Transfer call initiated successfully: {call_sid}")
|
||||
|
||||
# TODO: Possible race here between saving the transfer context
|
||||
# and getting a callback response from Twilio? Should we store_transfer_context
|
||||
# before sending request to Twilio and update the transfer context afterwards?
|
||||
|
||||
# Store transfer context in Redis
|
||||
call_transfer_manager = await get_call_transfer_manager()
|
||||
transfer_context = TransferContext(
|
||||
transfer_id=transfer_id,
|
||||
call_sid=call_sid,
|
||||
target_number=destination,
|
||||
tool_uuid=tool.tool_uuid,
|
||||
original_call_sid=original_call_sid,
|
||||
conference_name=conference_name,
|
||||
initiated_at=time.time(),
|
||||
)
|
||||
# Update transfer context with actual call_sid from provider response
|
||||
transfer_context.call_sid = call_sid
|
||||
await call_transfer_manager.store_transfer_context(transfer_context)
|
||||
|
||||
# Wait for status callback completion using Redis pub/sub
|
||||
|
|
@ -466,15 +481,15 @@ class CustomToolManager:
|
|||
transfer_event = None
|
||||
|
||||
finally:
|
||||
# Single cleanup point: stop hold music, unmute pipeline, remove context
|
||||
# Cleanup hold music and pipeline state
|
||||
# Transfer context cleanup is handled by respective transfer call strategies
|
||||
logger.info(
|
||||
"Transfer wait ended, cleaning up hold music, pipeline state, and transfer context"
|
||||
"Transfer wait ended, cleaning up hold music and pipeline state"
|
||||
)
|
||||
hold_music_stop_event.set()
|
||||
if hold_music_task:
|
||||
await hold_music_task
|
||||
self._engine.set_mute_pipeline(False)
|
||||
await call_transfer_manager.remove_transfer_context(transfer_id)
|
||||
|
||||
# Handle result (after cleanup)
|
||||
if transfer_event:
|
||||
|
|
@ -491,7 +506,6 @@ class CustomToolManager:
|
|||
"message": "I'm sorry, but the call is taking longer than expected to connect. The person might not be available right now. Please try calling back later.",
|
||||
"action": "transfer_failed",
|
||||
"reason": "timeout",
|
||||
"end_call": True,
|
||||
}
|
||||
await self._handle_transfer_result(
|
||||
timeout_result, function_call_params, properties
|
||||
|
|
@ -509,7 +523,6 @@ class CustomToolManager:
|
|||
"message": "I'm sorry, but something went wrong while trying to transfer your call. Please try again later or contact support if the problem persists.",
|
||||
"action": "transfer_failed",
|
||||
"reason": "execution_error",
|
||||
"end_call": True,
|
||||
}
|
||||
|
||||
await self._handle_transfer_result(
|
||||
|
|
@ -521,41 +534,52 @@ class CustomToolManager:
|
|||
async def _handle_transfer_result(
|
||||
self, result: dict, function_call_params, properties
|
||||
):
|
||||
"""Handle different transfer call outcomes and take appropriate action."""
|
||||
"""Handle transfer call outcomes from any telephony provider (Twilio, ARI, etc).
|
||||
|
||||
This method is provider-agnostic and processes standardized result dictionaries
|
||||
from transfer completion events, validation failures, timeouts, and errors.
|
||||
|
||||
Args:
|
||||
result: Standardized result dict with keys: action, status, reason, message
|
||||
function_call_params: LLM function call parameters for response callback
|
||||
properties: Function call result properties (e.g., run_llm setting)
|
||||
"""
|
||||
action = result.get("action", "")
|
||||
status = result.get("status", "")
|
||||
|
||||
logger.info(f"Handling transfer result: action={action}, status={status}")
|
||||
|
||||
if action == "transfer_success":
|
||||
# Successful transfer - add original caller to conference and end pipeline
|
||||
if action == "destination_answered":
|
||||
# Transfer destination answered - proceeding with bridge swap/conference join
|
||||
conference_id = result.get("conference_id")
|
||||
original_call_sid = result.get("original_call_sid")
|
||||
transfer_call_sid = result.get("transfer_call_sid")
|
||||
|
||||
logger.info(
|
||||
f"Transfer successful! Conference: {conference_id}, Original: {original_call_sid}, Transfer: {transfer_call_sid}"
|
||||
f"Transfer destination answered! Conference/Bridge: {conference_id}, "
|
||||
f"Original: {original_call_sid}, Transfer: {transfer_call_sid}"
|
||||
)
|
||||
|
||||
# Inform LLM of success and end the call with Transfer call reason
|
||||
# Inform LLM of success and end the call (no further LLM processing needed)
|
||||
response_properties = FunctionCallResultProperties(run_llm=False)
|
||||
await function_call_params.result_callback(
|
||||
{
|
||||
"status": "transfer_success",
|
||||
"message": "Transfer successful - connecting to conference",
|
||||
"message": "Transfer destination answered - connecting calls",
|
||||
"conference_id": conference_id,
|
||||
},
|
||||
properties=response_properties,
|
||||
)
|
||||
|
||||
# End pipeline - providers complete bridge swap/conference join as final transfer leg
|
||||
await self._engine.end_call_with_reason(
|
||||
EndTaskReason.TRANSFER_CALL.value, abort_immediately=False
|
||||
)
|
||||
|
||||
elif action == "transfer_failed":
|
||||
# Transfer failed - inform user via LLM and then end the call
|
||||
# Transfer failed - let LLM inform user with error details
|
||||
reason = result.get("reason", "unknown")
|
||||
logger.info(f"Transfer failed ({reason}), informing user")
|
||||
logger.info(f"Transfer failed ({reason}), informing user via LLM")
|
||||
|
||||
await function_call_params.result_callback(
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue