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