diff --git a/api/assets/hold-music-16000-mono.wav b/api/assets/hold-music-16000-mono.wav deleted file mode 100644 index e6b4a38..0000000 Binary files a/api/assets/hold-music-16000-mono.wav and /dev/null differ diff --git a/api/assets/hold-music-24000-mono.mp3 b/api/assets/hold-music-24000-mono.mp3 deleted file mode 100644 index 60a4526..0000000 Binary files a/api/assets/hold-music-24000-mono.mp3 and /dev/null differ diff --git a/api/assets/hold-music-8000-mono.wav b/api/assets/hold-music-8000-mono.wav deleted file mode 100644 index 73fa234..0000000 Binary files a/api/assets/hold-music-8000-mono.wav and /dev/null differ diff --git a/api/assets/hold-music-temp.wav b/api/assets/hold-music-temp.wav deleted file mode 100644 index 5b53ce5..0000000 Binary files a/api/assets/hold-music-temp.wav and /dev/null differ diff --git a/api/assets/office-ambience-16000-mono.wav b/api/assets/office-ambience-16000-mono.wav deleted file mode 100644 index e6b4a38..0000000 Binary files a/api/assets/office-ambience-16000-mono.wav and /dev/null differ diff --git a/api/assets/office-ambience-8000-mono.wav b/api/assets/office-ambience-8000-mono.wav deleted file mode 100644 index 73fa234..0000000 Binary files a/api/assets/office-ambience-8000-mono.wav and /dev/null differ diff --git a/api/routes/telephony.py b/api/routes/telephony.py index 4037382..a22ffa1 100644 --- a/api/routes/telephony.py +++ b/api/routes/telephony.py @@ -35,7 +35,7 @@ from api.services.auth.depends import get_user from api.services.campaign.campaign_call_dispatcher import campaign_call_dispatcher from api.services.campaign.campaign_event_publisher import get_campaign_event_publisher from api.services.quota_service import check_dograh_quota, check_dograh_quota_by_user_id -from api.services.telephony.transfer_coordination import get_transfer_coordinator +from api.services.telephony.call_transfer_manager import get_call_transfer_manager from api.services.telephony.transfer_event_protocol import TransferContext from api.services.telephony.factory import ( get_all_telephony_providers, @@ -53,13 +53,6 @@ from pipecat.utils.run_context import set_current_run_id router = APIRouter(prefix="/telephony") -# Module-level storage for webhook-driven function call completion -# Stores function call contexts that are waiting for webhook completion -pending_function_calls: Dict[str, tuple[FunctionCallParams, float]] = {} - -# Note: Transfer contexts now stored in Redis via TransferCoordinator -# pending_transfers dictionary removed in favor of Redis-based coordination - class InitiateCallRequest(BaseModel): workflow_id: int @@ -493,13 +486,12 @@ async def _validate_organization_provider_config( @router.post("/twiml", include_in_schema=False) async def handle_twiml_webhook( - workflow_id: int, user_id: int, workflow_run_id: int, organization_id: int, CallSid: str = Form(...) + workflow_id: int, user_id: int, workflow_run_id: int, organization_id: int ): """ Handle initial webhook from telephony provider. Returns provider-specific response (e.g., TwiML for Twilio). """ - logger.info(f"[TWIML-DEBUG] CallSid received: {CallSid}") provider = await get_telephony_provider(organization_id) @@ -1426,7 +1418,6 @@ async def handle_inbound_telephony( logger.info( f"Generated {normalized_data.provider} response for call {normalized_data.call_id}" ) - logger.info(f"response is {response}") return response except ValueError as e: @@ -1522,19 +1513,6 @@ async def handle_cloudonix_cdr(request: Request): return {"status": "success"} - -class CallTransferRequest(BaseModel): - """Request model for call transfer""" - target_phone_number: Optional[str] = None - phone_number: Optional[str] = None # Alternative field name - number: Optional[str] = None # Another alternative - current_call_sid: Optional[str] = None - - def get_target_number(self) -> str: - """Get the target phone number from any of the possible fields""" - return self.target_phone_number or self.phone_number or self.number or "" - - class TransferCallRequest(BaseModel): """Request model for initiating call transfer using webhook-driven completion""" destination: str # E.164 format phone number (required) @@ -1553,8 +1531,8 @@ class TransferCallRequest(BaseModel): if not destination or not destination.strip(): raise ValueError("Destination phone number is required") - e164_pattern = r"^\+[1-9]\d{1,14}$" - if not re.match(e164_pattern, destination.strip()): + E164_PHONE_REGEX = r"^\+[1-9]\d{1,14}$" + if not re.match(E164_PHONE_REGEX, destination.strip()): raise ValueError(f"Invalid phone number format: {destination}. Must be E.164 format (e.g., +1234567890)") return destination.strip() @@ -1566,27 +1544,18 @@ class TransferCallRequest(BaseModel): async def initiate_call_transfer(request: TransferCallRequest): """Initiate call transfer without blocking the pipeline""" import aiohttp - - logger.info(f"Received call transfer request: {request}") # Generate tool_call_id if not provided if not request.tool_call_id: request.tool_call_id = f"transfer_{int(time.time())}_{uuid.uuid4().hex[:8]}" - - # Log tool details for tracing - logger.info(f"Starting non-blocking call transfer to {request.destination} with tool_call_id: {request.tool_call_id}, tool_uuid: {request.tool_uuid}") - - # TODO: Add tool UUID validation here if needed - # For example: Validate that the tool UUID corresponds to a valid transfer call tool - # and that the destination matches the tool's configured destination pattern - + + logger.info(f"Starting call transfer to {request.destination} with tool_call_id: {request.tool_call_id}, tool_uuid: {request.tool_uuid}") + try: - # Get provider that supports transfers (validates Twilio-only requirement) from api.services.telephony.factory import get_transfer_provider try: provider = await get_transfer_provider(request.organization_id) except ValueError as e: - # Provider doesn't support transfers or organization not configured logger.error(f"Transfer provider validation failed: {e}") raise HTTPException( status_code=400, @@ -1597,7 +1566,7 @@ async def initiate_call_transfer(request: TransferCallRequest): if not provider.validate_config(): logger.error(f"Provider {provider.PROVIDER_NAME} configuration is invalid") raise HTTPException( - status_code=500, + status_code=400, detail=f"Telephony provider '{provider.PROVIDER_NAME}' is not properly configured for transfers" ) @@ -1610,7 +1579,7 @@ async def initiate_call_transfer(request: TransferCallRequest): timeout=request.timeout ) except NotImplementedError as e: - # This shouldn't happen due to get_transfer_provider validation, but safety check + # fallback for get_transfer_provider validation logger.error(f"Provider {provider.PROVIDER_NAME} doesn't support transfers: {e}") raise HTTPException( status_code=400, @@ -1626,10 +1595,10 @@ async def initiate_call_transfer(request: TransferCallRequest): call_sid = transfer_result.get("call_sid") logger.info(f"Transfer call initiated successfully: {call_sid}") - logger.info(f"Transfer result: {transfer_result}") + logger.debug(f"Transfer result: {transfer_result}") # Store the transfer context in Redis for webhook completion - transfer_coordinator = await get_transfer_coordinator() + call_transfer_manager = await get_call_transfer_manager() transfer_context = TransferContext( tool_call_id=request.tool_call_id, call_sid=call_sid, @@ -1637,12 +1606,10 @@ async def initiate_call_transfer(request: TransferCallRequest): tool_uuid=request.tool_uuid, original_call_sid=request.original_call_sid, caller_number=request.caller_number, - initiated_at=time.time(), - workflow_run_id=0 # TODO: Add workflow_run_id to request if needed + initiated_at=time.time() ) - await transfer_coordinator.store_transfer_context(transfer_context) + await call_transfer_manager.store_transfer_context(transfer_context) - # Return immediately without blocking return { "status": "transfer_initiated", "call_id": call_sid, @@ -1673,11 +1640,10 @@ async def handle_transfer_call_answered(tool_call_id: str, request: Request): call_sid = data.get("CallSid", "") # Get transfer context from Redis - transfer_coordinator = await get_transfer_coordinator() - transfer_context = await transfer_coordinator.get_transfer_context(tool_call_id) + call_transfer_manager = await get_call_transfer_manager() + transfer_context = await call_transfer_manager.get_transfer_context(tool_call_id) original_call_sid = transfer_context.original_call_sid if transfer_context else None - caller_number = transfer_context.caller_number if transfer_context else None # Use original call SID for conference name if available, otherwise fall back to transfer call SID base_call_sid = original_call_sid or call_sid @@ -1688,8 +1654,8 @@ async def handle_transfer_call_answered(tool_call_id: str, request: Request): # Publish Redis event for transfer answer completion try: # Get transfer coordinator and context - transfer_coordinator = await get_transfer_coordinator() - transfer_context = await transfer_coordinator.get_transfer_context(tool_call_id) + call_transfer_manager = await get_call_transfer_manager() + transfer_context = await call_transfer_manager.get_transfer_context(tool_call_id) if transfer_context: # Create transfer answered event @@ -1698,17 +1664,16 @@ async def handle_transfer_call_answered(tool_call_id: str, request: Request): transfer_event = TransferEvent( type=TransferEventType.TRANSFER_ANSWERED, tool_call_id=tool_call_id, - workflow_run_id=transfer_context.workflow_run_id, original_call_sid=original_call_sid, transfer_call_sid=call_sid, conference_name=conference_name, - message="Great! The person answered. Let me transfer you now.", + message="Great! The destination number answered. Let me transfer you now.", status="success", action="transfer_success" ) # Publish the event to Redis - await transfer_coordinator.publish_transfer_event(transfer_event) + await call_transfer_manager.publish_transfer_event(transfer_event) logger.info(f"Published TRANSFER_ANSWERED event for {tool_call_id}") else: @@ -1738,9 +1703,7 @@ async def complete_transfer_function_call(tool_call_id: str, request: Request): call_status = data.get("CallStatus", "") call_sid = data.get("CallSid", "") - logger.info(f"Transfer result webhook: {tool_call_id} status={call_status}") - - # Note: All transfer coordination now handled via Redis events + logger.info(f"Transfer result(call status) webhook: {tool_call_id} status={call_status}") # Skip "completed" status to avoid overriding successful transfer results # The "answered" status already handled the success case @@ -1752,13 +1715,12 @@ async def complete_transfer_function_call(tool_call_id: str, request: Request): from api.services.telephony.transfer_event_protocol import TransferEvent, TransferEventType # Get transfer context from Redis for additional information - transfer_coordinator = await get_transfer_coordinator() - transfer_context = await transfer_coordinator.get_transfer_context(tool_call_id) + call_transfer_manager = await get_call_transfer_manager() + transfer_context = await call_transfer_manager.get_transfer_context(tool_call_id) original_call_sid = transfer_context.original_call_sid if transfer_context else None caller_number = transfer_context.caller_number if transfer_context else None - - + # Determine the result based on call status with user-friendly messaging if call_status == "answered": # Use original call SID for conference name if available, otherwise fall back to transfer call SID @@ -1767,7 +1729,7 @@ async def complete_transfer_function_call(tool_call_id: str, request: Request): result = { "status": "success", - "message": "Great! The person answered. Let me transfer you now.", + "message": "Great! The destination number answered. Let me transfer you now.", "action": "transfer_success", "conference_id": conference_name, "transfer_call_sid": call_sid, # The outbound transfer call SID @@ -1825,7 +1787,6 @@ async def complete_transfer_function_call(tool_call_id: str, request: Request): transfer_event = TransferEvent( type=event_type, tool_call_id=tool_call_id, - workflow_run_id=0, # TODO: Extract from context if needed original_call_sid=original_call_sid or "", transfer_call_sid=call_sid, conference_name=result.get("conference_id"), @@ -1837,12 +1798,12 @@ async def complete_transfer_function_call(tool_call_id: str, request: Request): ) # Publish the event via Redis - await transfer_coordinator.publish_transfer_event(transfer_event) + await call_transfer_manager.publish_transfer_event(transfer_event) logger.info(f"Published {event_type} event for {tool_call_id}") # Clean up transfer context from Redis - await transfer_coordinator.remove_transfer_context(tool_call_id) + await call_transfer_manager.remove_transfer_context(tool_call_id) logger.info(f"Function call {tool_call_id} completed with result: {result['status']}") @@ -1852,23 +1813,6 @@ async def complete_transfer_function_call(tool_call_id: str, request: Request): return {"status": "completed", "result": result} -@router.post("/register-transfer-tool-call") -async def register_transfer_tool_call(request: Request): - """Register a pending transfer function call for webhook completion""" - data = await request.json() - - tool_call_id = data.get("tool_call_id") - function_call_params = data.get("function_call_params") - - if not tool_call_id or not function_call_params: - raise HTTPException(status_code=400, detail="Missing required fields") - - # Store the function call context for webhook completion - pending_function_calls[tool_call_id] = (function_call_params, time.time()) - - logger.info(f"Registered transfer tool call: {tool_call_id}") - - return {"status": "registered", "tool_call_id": tool_call_id} \ No newline at end of file diff --git a/api/services/telephony/transfer_coordination.py b/api/services/telephony/call_transfer_manager.py similarity index 93% rename from api/services/telephony/transfer_coordination.py rename to api/services/telephony/call_transfer_manager.py index f5e08b4..cd8bfb7 100644 --- a/api/services/telephony/transfer_coordination.py +++ b/api/services/telephony/call_transfer_manager.py @@ -19,8 +19,8 @@ from api.services.telephony.transfer_event_protocol import ( ) -class TransferCoordinator: - """Coordinates transfer events and context storage using Redis.""" +class CallTransferManager: + """Manages call transfer events and context storage using Redis.""" def __init__(self, redis_client: Optional[aioredis.Redis] = None): self._redis_client = redis_client @@ -155,7 +155,6 @@ class TransferCoordinator: timeout_event = TransferEvent( type=TransferEventType.TRANSFER_TIMEOUT, tool_call_id=tool_call_id, - workflow_run_id=0, # Will be updated by caller original_call_sid="", status="failed", reason="timeout", @@ -192,13 +191,13 @@ class TransferCoordinator: logger.error(f"Error during transfer coordinator cleanup: {e}") -# Global transfer coordinator instance -_transfer_coordinator: Optional[TransferCoordinator] = None +# Global call transfer manager instance +_call_transfer_manager: Optional[CallTransferManager] = None -async def get_transfer_coordinator() -> TransferCoordinator: - """Get or create the global transfer coordinator instance.""" - global _transfer_coordinator - if not _transfer_coordinator: - _transfer_coordinator = TransferCoordinator() - return _transfer_coordinator \ No newline at end of file +async def get_call_transfer_manager() -> CallTransferManager: + """Get or create the global call transfer manager instance.""" + global _call_transfer_manager + if not _call_transfer_manager: + _call_transfer_manager = CallTransferManager() + return _call_transfer_manager \ No newline at end of file diff --git a/api/services/telephony/transfer_event_protocol.py b/api/services/telephony/transfer_event_protocol.py index 88c481b..6b2ef90 100644 --- a/api/services/telephony/transfer_event_protocol.py +++ b/api/services/telephony/transfer_event_protocol.py @@ -27,7 +27,6 @@ class TransferEvent: type: TransferEventType tool_call_id: str - workflow_run_id: int original_call_sid: str transfer_call_sid: Optional[str] = None target_number: Optional[str] = None @@ -75,7 +74,6 @@ class TransferContext: original_call_sid: str caller_number: Optional[str] initiated_at: float - workflow_run_id: int def to_json(self) -> str: """Convert context to JSON string.""" diff --git a/api/services/telephony/transfer_test.py b/api/services/telephony/transfer_test.py index 6885949..6441c8b 100644 --- a/api/services/telephony/transfer_test.py +++ b/api/services/telephony/transfer_test.py @@ -11,7 +11,7 @@ from typing import Optional from loguru import logger -from api.services.telephony.transfer_coordination import get_transfer_coordinator +from api.services.telephony.call_transfer_manager import get_call_transfer_manager from api.services.telephony.transfer_event_protocol import ( TransferContext, TransferEvent, @@ -23,7 +23,7 @@ async def test_redis_coordination(): """Test basic Redis pub/sub coordination for transfers.""" logger.info("Testing Redis-based transfer coordination...") - transfer_coordinator = await get_transfer_coordinator() + call_transfer_manager = await get_call_transfer_manager() # Test 1: Store and retrieve transfer context tool_call_id = str(uuid.uuid4()) @@ -34,15 +34,14 @@ async def test_redis_coordination(): tool_uuid="test_tool_uuid", original_call_sid="original_call_123", caller_number="+0987654321", - initiated_at=time.time(), - workflow_run_id=123 + initiated_at=time.time() ) logger.info("Test 1: Storing transfer context...") - await transfer_coordinator.store_transfer_context(test_context) + await call_transfer_manager.store_transfer_context(test_context) logger.info("Test 1: Retrieving transfer context...") - retrieved_context = await transfer_coordinator.get_transfer_context(tool_call_id) + retrieved_context = await call_transfer_manager.get_transfer_context(tool_call_id) if retrieved_context and retrieved_context.tool_call_id == tool_call_id: logger.info("✅ Test 1 PASSED: Context storage/retrieval works") @@ -55,7 +54,7 @@ async def test_redis_coordination(): # Start waiting for completion in background async def wait_for_completion(): - return await transfer_coordinator.wait_for_transfer_completion(tool_call_id, 5.0) + return await call_transfer_manager.wait_for_transfer_completion(tool_call_id, 5.0) wait_task = asyncio.create_task(wait_for_completion()) @@ -66,7 +65,6 @@ async def test_redis_coordination(): test_event = TransferEvent( type=TransferEventType.TRANSFER_COMPLETED, tool_call_id=tool_call_id, - workflow_run_id=123, original_call_sid="original_call_123", transfer_call_sid="transfer_call_456", conference_name="test-conference", @@ -76,7 +74,7 @@ async def test_redis_coordination(): ) logger.info("Test 2: Publishing completion event...") - await transfer_coordinator.publish_transfer_event(test_event) + await call_transfer_manager.publish_transfer_event(test_event) # Wait for the completion received_event = await wait_task @@ -89,9 +87,9 @@ async def test_redis_coordination(): # Test 3: Cleanup logger.info("Test 3: Testing cleanup...") - await transfer_coordinator.remove_transfer_context(tool_call_id) + await call_transfer_manager.remove_transfer_context(tool_call_id) - cleanup_context = await transfer_coordinator.get_transfer_context(tool_call_id) + cleanup_context = await call_transfer_manager.get_transfer_context(tool_call_id) if cleanup_context is None: logger.info("✅ Test 3 PASSED: Cleanup works") else: @@ -106,12 +104,12 @@ async def test_timeout_handling(): """Test timeout handling in transfer coordination.""" logger.info("Testing timeout handling...") - transfer_coordinator = await get_transfer_coordinator() + call_transfer_manager = await get_call_transfer_manager() tool_call_id = str(uuid.uuid4()) # Wait for completion with short timeout (should timeout) start_time = time.time() - result = await transfer_coordinator.wait_for_transfer_completion(tool_call_id, 2.0) + result = await call_transfer_manager.wait_for_transfer_completion(tool_call_id, 2.0) elapsed = time.time() - start_time if result is None and elapsed >= 2.0: diff --git a/api/services/workflow/pipecat_engine_custom_tools.py b/api/services/workflow/pipecat_engine_custom_tools.py index 038c121..5c43016 100644 --- a/api/services/workflow/pipecat_engine_custom_tools.py +++ b/api/services/workflow/pipecat_engine_custom_tools.py @@ -35,7 +35,7 @@ from pipecat.utils.enums import EndTaskReason from pipecat.transports.websocket.fastapi import FastAPIWebsocketClient from api.utils.hold_audio import load_hold_audio -from api.services.telephony.transfer_coordination import get_transfer_coordinator +from api.services.telephony.call_transfer_manager import get_call_transfer_manager from api.services.telephony.transfer_event_protocol import ( TransferEvent, TransferContext, @@ -359,7 +359,7 @@ class CustomToolManager: backend_url, _ = await get_backend_endpoints() # Get transfer coordinator for Redis-based coordination - transfer_coordinator = await get_transfer_coordinator() + call_transfer_manager = await get_call_transfer_manager() # Now initiate the transfer call transfer_url = f"{backend_url}/api/v1/telephony/call-transfer" @@ -415,7 +415,7 @@ class CustomToolManager: "Waiting for transfer completion via Redis pub/sub..." ) transfer_event = ( - await transfer_coordinator.wait_for_transfer_completion( + await call_transfer_manager.wait_for_transfer_completion( transfer_data["tool_call_id"], timeout_seconds ) ) @@ -435,7 +435,7 @@ class CustomToolManager: # Get transfer context for caller number transfer_context = ( - await transfer_coordinator.get_transfer_context( + await call_transfer_manager.get_transfer_context( transfer_data["tool_call_id"] ) ) diff --git a/api/tasks/arq.py b/api/tasks/arq.py index 683ed80..c796a6d 100644 --- a/api/tasks/arq.py +++ b/api/tasks/arq.py @@ -51,7 +51,6 @@ from api.tasks.s3_upload import ( process_workflow_completion, upload_voicemail_audio_to_s3, ) -from api.tasks.transfer_handler import handle_transfer_redirect class WorkerSettings: @@ -63,7 +62,6 @@ class WorkerSettings: sync_campaign_source, process_campaign_batch, process_knowledge_base_document, - handle_transfer_redirect, ] cron_jobs = [] redis_settings = REDIS_SETTINGS diff --git a/api/tasks/function_names.py b/api/tasks/function_names.py index 4f96148..af92c7c 100644 --- a/api/tasks/function_names.py +++ b/api/tasks/function_names.py @@ -6,4 +6,4 @@ class FunctionNames: SYNC_CAMPAIGN_SOURCE = "sync_campaign_source" PROCESS_CAMPAIGN_BATCH = "process_campaign_batch" PROCESS_KNOWLEDGE_BASE_DOCUMENT = "process_knowledge_base_document" - HANDLE_TRANSFER_REDIRECT = "handle_transfer_redirect" + diff --git a/api/tasks/transfer_handler.py b/api/tasks/transfer_handler.py deleted file mode 100644 index dcfe011..0000000 --- a/api/tasks/transfer_handler.py +++ /dev/null @@ -1,137 +0,0 @@ -""" -ARQ task to handle call transfer redirect independently from pipeline. - -This task runs in a separate worker process, ensuring the transfer logic -is completely decoupled from the real-time audio pipeline. -""" - -import asyncio -import aiohttp -from loguru import logger - -from api.utils.common import get_backend_endpoints - - -async def handle_transfer_redirect( - ctx, - original_call_sid: str, - conference_id: str, - transfer_call_sid: str -): - """ - Handle call transfer redirect in ARQ worker, independent of pipeline. - - Following the test bench approach: - 1. Wait for WebSocket closure to complete - 2. Verify conference state (destination still connected) - 3. Redirect original caller to conference using TwiML endpoint - 4. Handle any failures gracefully - - Args: - original_call_sid: The original caller's Twilio call SID - conference_id: The conference name to join caller to - transfer_call_sid: The destination call SID (for verification) - """ - logger.info("=" * 60) - logger.info("🚀 ARQ TRANSFER REDIRECT STARTED") - logger.info(f" Original Caller SID: {original_call_sid}") - logger.info(f" Conference ID: {conference_id}") - logger.info(f" Destination Call SID: {transfer_call_sid}") - logger.info("=" * 60) - - try: - # Step 1: Wait for WebSocket closure to complete (test bench approach) - logger.info("⏱️ Step 1: Waiting for WebSocket closure to complete...") - await asyncio.sleep(2.0) # Test bench uses 1.5s, we use 2s for safety - logger.info(" WebSocket closure wait completed") - - # Step 2: Verify destination is still in conference (test bench approach) - logger.info("🔍 Step 2: Verifying destination is still in conference...") - try: - # TODO: Add actual Twilio conference verification here - # For now, assume destination is still connected - logger.info(" Destination verification completed (assuming connected)") - except Exception as e: - logger.warning(f" Could not verify destination: {e}") - - # Step 3: Redirect caller to conference (test bench approach) - logger.info("📞 Step 3: Redirecting caller to conference...") - - success = await _redirect_caller_to_conference(original_call_sid, conference_id) - - if success: - logger.info("✅ TRANSFER REDIRECT SUCCESSFUL!") - logger.info(" Caller should now be in conference with destination") - else: - logger.error("❌ TRANSFER REDIRECT FAILED!") - - except Exception as e: - logger.exception(f"❌ Transfer redirect error: {e}") - - logger.info("=" * 60) - logger.info("🏁 ARQ TRANSFER REDIRECT COMPLETED") - logger.info("=" * 60) - - -async def _redirect_caller_to_conference(call_sid: str, conference_name: str) -> bool: - """ - Redirect caller to conference using Twilio API. - - Exactly following the test bench approach. - - Args: - call_sid: Twilio call SID to redirect - conference_name: Name of the conference to join - - Returns: - bool: True if redirect was successful, False otherwise - """ - logger.info(f"[TRANSFER-DEBUG] _redirect_caller_to_conference called with: {call_sid} and {conference_name}") - - # TODO: Use provider service in production instead of hardcoded credentials - account_sid = "" - auth_token = "" - - try: - # Get public backend endpoint for TwiML URL - backend_endpoint, _ = await get_backend_endpoints() - - # Construct TwiML endpoint URL - transfer_url = f"{backend_endpoint}/api/v1/telephony/transfer-twiml/{conference_name}" - - logger.info(f"[TRANSFER-DEBUG] Transfer URL: {transfer_url}") - - # Twilio API endpoint for updating calls - api_endpoint = f"https://api.twilio.com/2010-04-01/Accounts/{account_sid}/Calls/{call_sid}.json" - - # Redirect data - exactly like test bench - redirect_data = { - "url": transfer_url, - "method": "POST" - } - - logger.info(f"[TRANSFER-DEBUG] Redirecting caller {call_sid} to conference {conference_name}") - logger.info(f"[TRANSFER-DEBUG] API endpoint: {api_endpoint}") - logger.info(f"[TRANSFER-DEBUG] Redirect data: {redirect_data}") - - # Make the redirect API call - async with aiohttp.ClientSession() as session: - logger.info(f"[TRANSFER-DEBUG] Created aiohttp session") - auth = aiohttp.BasicAuth(account_sid, auth_token) - logger.info(f"[TRANSFER-DEBUG] Making POST request to Twilio API for redirect") - - async with session.post(api_endpoint, data=redirect_data, auth=auth) as response: - logger.info(f"[TRANSFER-DEBUG] Received response from Twilio API") - - if response.status == 200: - logger.info(f"[TRANSFER-DEBUG] API response status: 200") - logger.info(f"[TRANSFER-DEBUG] Successfully redirected caller to conference {conference_name}") - return True - else: - error_text = await response.text() - logger.error(f"[TRANSFER-DEBUG] Redirect failed - Status: {response.status}, Response: {error_text}") - return False - - except Exception as e: - logger.exception(f"[TRANSFER-DEBUG] Exception during redirect: {e}") - return False \ No newline at end of file