dograh/api/services/pipecat/pre_call_fetch.py
2026-06-05 14:16:56 +05:30

130 lines
4.4 KiB
Python

"""Pre-call HTTP data fetch for StartCall node.
Executes an HTTP request before a voice call starts to enrich the
call context with data from external systems (CRM, ERP, etc.).
"""
from typing import Any, Dict, Optional
import httpx
from loguru import logger
from api.db import db_client
from api.utils.credential_auth import build_auth_header
PRE_CALL_FETCH_TIMEOUT_SECONDS = 10
def _extract_initial_context(response_data: Dict[str, Any]) -> Dict[str, Any]:
"""Pull the context variables out of a pre-call fetch response.
The canonical key is ``initial_context``. The legacy ``dynamic_variables``
key is still accepted for backward compatibility, so existing endpoints
keep working; ``initial_context`` takes precedence when both are present.
Either key may appear at the top level or nested under ``call_inbound``:
{"call_inbound": {"initial_context": {...}}} | {"initial_context": {...}}
{"call_inbound": {"dynamic_variables": {...}}} | {"dynamic_variables": {...}}
"""
container = response_data.get("call_inbound")
if not isinstance(container, dict):
container = response_data
for key in ("initial_context", "dynamic_variables"):
value = container.get(key)
if isinstance(value, dict):
return value
return {}
async def execute_pre_call_fetch(
*,
url: str,
credential_uuid: Optional[str],
call_context_vars: Dict[str, Any],
workflow_id: int,
organization_id: int,
) -> Dict[str, Any]:
"""Execute a POST request to fetch data before a call starts.
Sends a standardized payload with call metadata (agent_id, from/to numbers).
The response JSON is returned as a dict to be merged into initial_context.
Returns:
Response JSON dict on success, empty dict on any failure.
Never raises.
"""
# Build standardized payload
payload = {
"event": "call_inbound",
"call_inbound": {
"agent_id": workflow_id,
"from_number": call_context_vars.get("caller_number", ""),
"to_number": call_context_vars.get("called_number", ""),
},
}
# Build headers
headers: Dict[str, str] = {"Content-Type": "application/json"}
if credential_uuid:
try:
credential = await db_client.get_credential_by_uuid(
credential_uuid, organization_id
)
if credential:
headers.update(build_auth_header(credential))
else:
logger.warning(
f"Pre-call fetch: credential {credential_uuid} not found"
)
except Exception as e:
logger.error(f"Pre-call fetch: failed to resolve credential: {e}")
logger.info(f"Pre-call fetch: POST {url}")
try:
async with httpx.AsyncClient(timeout=PRE_CALL_FETCH_TIMEOUT_SECONDS) as client:
response = await client.post(url, headers=headers, json=payload)
try:
response_data = response.json()
except Exception:
response_data = {}
if response.is_success:
if not isinstance(response_data, dict):
logger.warning(
"Pre-call fetch: response is not a JSON object, skipping"
)
return {}
# Extract the variables to merge into initial_context. Prefers
# the canonical `initial_context` key, falling back to the
# legacy `dynamic_variables` key for backward compatibility.
initial_context_vars = _extract_initial_context(response_data)
logger.info(
f"Pre-call fetch: success ({response.status_code}), "
f"initial_context keys: {list(initial_context_vars.keys())}"
)
return initial_context_vars
else:
logger.warning(
f"Pre-call fetch: HTTP {response.status_code} - "
f"{response.text[:200]}"
)
return {}
except httpx.TimeoutException:
logger.error(
f"Pre-call fetch: timed out after {PRE_CALL_FETCH_TIMEOUT_SECONDS}s"
)
return {}
except httpx.RequestError as e:
logger.error(f"Pre-call fetch: request failed: {e}")
return {}
except Exception as e:
logger.error(f"Pre-call fetch: unexpected error: {e}")
return {}