mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
chore: refactor telephony
This commit is contained in:
parent
e485f649bd
commit
942a20bd14
14 changed files with 52 additions and 252 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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}
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
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
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue