diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..3fdd478
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,6 @@
+{
+ "effortLevel": "high",
+ "env": {
+ "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
+ }
+}
diff --git a/api/alembic/versions/1a7d74d54e8f_add_transfer_call_tool_category.py b/api/alembic/versions/1a7d74d54e8f_add_transfer_call_tool_category.py
index de3e765..0038dbe 100644
--- a/api/alembic/versions/1a7d74d54e8f_add_transfer_call_tool_category.py
+++ b/api/alembic/versions/1a7d74d54e8f_add_transfer_call_tool_category.py
@@ -5,15 +5,15 @@ Revises: 02ffd7f23d1d
Create Date: 2026-02-03 11:18:11.417837
"""
+
from typing import Sequence, Union
from alembic import op
from alembic_postgresql_enum import TableReference
-
# revision identifiers, used by Alembic.
-revision: str = '1a7d74d54e8f'
-down_revision: Union[str, None] = '02ffd7f23d1d'
+revision: str = "1a7d74d54e8f"
+down_revision: Union[str, None] = "02ffd7f23d1d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
diff --git a/api/routes/telephony.py b/api/routes/telephony.py
index a22ffa1..43ee771 100644
--- a/api/routes/telephony.py
+++ b/api/routes/telephony.py
@@ -4,15 +4,13 @@ Consolidated from split modules for easier maintenance.
"""
import json
-import time
import uuid
from datetime import UTC, datetime
-from typing import Dict, Optional
+from typing import Optional
from fastapi import (
APIRouter,
Depends,
- Form,
Header,
HTTPException,
Request,
@@ -36,11 +34,14 @@ from api.services.campaign.campaign_call_dispatcher import campaign_call_dispatc
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.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,
get_telephony_provider,
)
+from api.services.telephony.transfer_event_protocol import (
+ TransferEvent,
+ TransferEventType,
+)
from api.utils.common import get_backend_endpoints
from api.utils.telephony_helper import (
generic_hangup_response,
@@ -48,7 +49,6 @@ from api.utils.telephony_helper import (
numbers_match,
parse_webhook_request,
)
-from pipecat.services.llm_service import FunctionCallParams
from pipecat.utils.run_context import set_current_run_id
router = APIRouter(prefix="/telephony")
@@ -509,15 +509,15 @@ async def transfer_twiml(conference_name: str):
Called by Twilio when we redirect the call after closing the WebSocket stream.
"""
logger.info(f"[TRANSFER-TWIML] Generating conference TwiML for: {conference_name}")
-
+
twiml_content = f"""
Connecting you now.
- {conference_name}
+ {conference_name}
"""
-
+
logger.info(f"[TRANSFER-TWIML] Generated TwiML: {twiml_content}")
return HTMLResponse(content=twiml_content, media_type="application/xml")
@@ -1460,6 +1460,8 @@ async def handle_inbound_fallback(request: Request):
)
return generic_hangup_response()
+
+
@router.post("/cloudonix/cdr")
async def handle_cloudonix_cdr(request: Request):
"""Handle Cloudonix CDR (Call Detail Record) webhooks.
@@ -1510,223 +1512,138 @@ async def handle_cloudonix_cdr(request: Request):
f"disposition: {cdr_data.get('disposition')}, status: {status_update.status}"
)
-
return {"status": "success"}
+
class TransferCallRequest(BaseModel):
- """Request model for initiating call transfer using webhook-driven completion"""
+ """Request model for initiating a call transfer."""
+
destination: str # E.164 format phone number (required)
organization_id: int # Organization ID for provider configuration
+ transfer_id: str # Unique identifier for tracking this transfer
+ conference_name: str # Conference name for the transfer
timeout: Optional[int] = 20 # seconds to wait for answer
- tool_call_id: Optional[str] = None # will generate if not provided
- tool_uuid: Optional[str] = None # tool UUID for tracing and validation
- original_call_sid: Optional[str] = None # original caller's call SID
- caller_number: Optional[str] = None # original caller's phone number
@field_validator("destination")
@classmethod
def validate_destination(cls, destination: str) -> str:
"""Validate destination is in E.164 format."""
import re
+
if not destination or not destination.strip():
raise ValueError("Destination phone number is required")
-
+
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)")
-
+ raise ValueError(
+ f"Invalid phone number format: {destination}. Must be E.164 format (e.g., +1234567890)"
+ )
+
return destination.strip()
-
-
@router.post("/call-transfer")
async def initiate_call_transfer(request: TransferCallRequest):
- """Initiate call transfer without blocking the pipeline"""
- import aiohttp
- # 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]}"
-
- logger.info(f"Starting call transfer to {request.destination} with tool_call_id: {request.tool_call_id}, tool_uuid: {request.tool_uuid}")
-
+ """Initiate a call transfer via the telephony provider.
+
+ This endpoint only initiates the outbound call. Transfer context
+ (original_call_sid, etc.) is stored by the caller
+ before invoking this endpoint.
+ """
+ logger.info(
+ f"Starting call transfer to {request.destination} with transfer_id: {request.transfer_id}"
+ )
+
try:
- from api.services.telephony.factory import get_transfer_provider
-
try:
- provider = await get_transfer_provider(request.organization_id)
+ provider = await get_telephony_provider(request.organization_id)
except ValueError as e:
logger.error(f"Transfer provider validation failed: {e}")
raise HTTPException(
- status_code=400,
- detail=f"Call transfer not supported: {str(e)}"
+ status_code=400, detail=f"Call transfer not supported: {str(e)}"
)
-
- # Validate configuration before attempting transfer
+
+ if not provider.supports_transfers():
+ raise HTTPException(
+ status_code=400,
+ detail=f"Provider '{provider.PROVIDER_NAME}' does not support call transfers",
+ )
+
if not provider.validate_config():
logger.error(f"Provider {provider.PROVIDER_NAME} configuration is invalid")
raise HTTPException(
status_code=400,
- detail=f"Telephony provider '{provider.PROVIDER_NAME}' is not properly configured for transfers"
+ detail=f"Telephony provider '{provider.PROVIDER_NAME}' is not properly configured for transfers",
)
-
- # Initiate transfer call via provider
+
logger.info(f"Initiating transfer call via {provider.PROVIDER_NAME} provider")
try:
transfer_result = await provider.transfer_call(
destination=request.destination,
- tool_call_id=request.tool_call_id,
- timeout=request.timeout
+ transfer_id=request.transfer_id,
+ conference_name=request.conference_name,
+ timeout=request.timeout,
)
except NotImplementedError as e:
- # fallback for get_transfer_provider validation
- logger.error(f"Provider {provider.PROVIDER_NAME} doesn't support transfers: {e}")
+ logger.error(
+ f"Provider {provider.PROVIDER_NAME} doesn't support transfers: {e}"
+ )
raise HTTPException(
status_code=400,
- detail=f"Provider '{provider.PROVIDER_NAME}' does not support call transfers"
+ detail=f"Provider '{provider.PROVIDER_NAME}' does not support call transfers",
)
except Exception as e:
- # Provider API call failed
logger.error(f"Provider transfer call failed: {e}")
raise HTTPException(
- status_code=500,
- detail=f"Transfer call failed: {str(e)}"
+ status_code=500, detail=f"Transfer call failed: {str(e)}"
)
-
+
call_sid = transfer_result.get("call_sid")
logger.info(f"Transfer call initiated successfully: {call_sid}")
logger.debug(f"Transfer result: {transfer_result}")
-
- # Store the transfer context in Redis for webhook completion
- call_transfer_manager = await get_call_transfer_manager()
- transfer_context = TransferContext(
- tool_call_id=request.tool_call_id,
- call_sid=call_sid,
- target_number=request.destination,
- tool_uuid=request.tool_uuid,
- original_call_sid=request.original_call_sid,
- caller_number=request.caller_number,
- initiated_at=time.time()
- )
- await call_transfer_manager.store_transfer_context(transfer_context)
-
+
return {
- "status": "transfer_initiated",
+ "status": "transfer_initiated",
"call_id": call_sid,
"message": f"Calling {request.destination}...",
- "tool_call_id": request.tool_call_id,
- "provider": provider.PROVIDER_NAME
+ "transfer_id": request.transfer_id,
+ "provider": provider.PROVIDER_NAME,
}
-
+
except HTTPException:
- # Re-raise HTTP exceptions (already properly formatted)
raise
except Exception as e:
- # Catch any other unexpected errors
logger.error(f"Unexpected error during transfer call: {e}")
raise HTTPException(
- status_code=500,
- detail=f"Internal error during transfer: {str(e)}"
+ status_code=500, detail=f"Internal error during transfer: {str(e)}"
)
-@router.post("/transfer-call-handler/{tool_call_id}")
-async def handle_transfer_call_answered(tool_call_id: str, request: Request):
- """Handle when target answers the transfer call"""
- logger.info(f"Transfer call answered for tool_call_id: {tool_call_id}")
-
+@router.post("/transfer-result/{transfer_id}")
+async def complete_transfer_function_call(transfer_id: str, request: Request):
+ """Webhook endpoint to complete the function call with transfer result.
+
+ Called by Twilio's StatusCallback when the transfer call status changes.
+ """
form_data = await request.form()
data = dict(form_data)
- call_sid = data.get("CallSid", "")
-
- # Get transfer context from Redis
- 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
-
- # 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
- conference_name = f"transfer-{base_call_sid}"
-
- logger.info(f"Using conference name: {conference_name}")
-
- # Publish Redis event for transfer answer completion
- try:
- # Get transfer coordinator and context
- 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
- from api.services.telephony.transfer_event_protocol import TransferEvent, TransferEventType
-
- transfer_event = TransferEvent(
- type=TransferEventType.TRANSFER_ANSWERED,
- tool_call_id=tool_call_id,
- original_call_sid=original_call_sid,
- transfer_call_sid=call_sid,
- conference_name=conference_name,
- message="Great! The destination number answered. Let me transfer you now.",
- status="success",
- action="transfer_success"
- )
-
- # Publish the event to Redis
- await call_transfer_manager.publish_transfer_event(transfer_event)
- logger.info(f"Published TRANSFER_ANSWERED event for {tool_call_id}")
-
- else:
- logger.warning(f"No transfer context found for {tool_call_id}")
-
- except Exception as e:
- logger.error(f"Error publishing transfer answered event for {tool_call_id}: {e}")
-
- # Return TwiML to put the answerer into the conference
- twiml = f"""
-
- You have answered a transfer call. Connecting you now.
-
- {conference_name}
-
-"""
-
- return HTMLResponse(content=twiml, media_type="application/xml")
-
-@router.post("/transfer-result/{tool_call_id}")
-async def complete_transfer_function_call(tool_call_id: str, request: Request):
- """Webhook endpoint to complete the function call with transfer result"""
- form_data = await request.form()
- data = dict(form_data)
-
call_status = data.get("CallStatus", "")
call_sid = data.get("CallSid", "")
-
- 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
- if call_status == "completed":
- logger.info(f"Ignoring 'completed' status for {tool_call_id} to avoid overriding previous results")
- return {"status": "ignored", "reason": "completed_status_filtered"}
-
- # Import required event classes
- from api.services.telephony.transfer_event_protocol import TransferEvent, TransferEventType
-
+
+ logger.info(
+ f"Transfer result(call status) webhook: {transfer_id} status={call_status}"
+ )
+
# Get transfer context from Redis for additional information
call_transfer_manager = await get_call_transfer_manager()
- transfer_context = await call_transfer_manager.get_transfer_context(tool_call_id)
-
+ transfer_context = await call_transfer_manager.get_transfer_context(transfer_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
-
+ conference_name = transfer_context.conference_name 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
- base_call_sid = original_call_sid or call_sid
- conference_name = f"transfer-{base_call_sid}"
-
+ if call_status in ("answered", "completed"):
result = {
"status": "success",
"message": "Great! The destination number answered. Let me transfer you now.",
@@ -1734,8 +1651,7 @@ async def complete_transfer_function_call(tool_call_id: str, request: Request):
"conference_id": conference_name,
"transfer_call_sid": call_sid, # The outbound transfer call SID
"original_call_sid": original_call_sid, # The original caller's SID
- "caller_number": caller_number,
- "end_call": False # Continue with transfer
+ "end_call": False, # Continue with transfer
}
elif call_status == "no-answer":
result = {
@@ -1744,31 +1660,33 @@ async def complete_transfer_function_call(tool_call_id: str, request: Request):
"message": "The transfer call was not answered. The person may be busy or unavailable right now.",
"action": "transfer_failed",
"call_sid": call_sid,
- "end_call": True
+ "end_call": True,
}
elif call_status == "busy":
result = {
- "status": "transfer_failed",
+ "status": "transfer_failed",
"reason": "busy",
"message": "The transfer call encountered a busy signal. The person is likely on another call.",
"action": "transfer_failed",
"call_sid": call_sid,
- "end_call": True
+ "end_call": True,
}
elif call_status == "failed":
result = {
"status": "transfer_failed",
"reason": "call_failed",
"message": "The transfer call failed to connect. There may be a network issue or the number is unavailable.",
- "action": "transfer_failed",
+ "action": "transfer_failed",
"call_sid": call_sid,
- "end_call": True
+ "end_call": True,
}
else:
# Intermediate status (ringing, in-progress, etc.), don't complete yet
- logger.info(f"Received intermediate status {call_status}, waiting for final status")
+ logger.info(
+ f"Received intermediate status {call_status}, waiting for final status"
+ )
return {"status": "pending"}
-
+
# Complete the function call with Redis event publishing
try:
# Determine event type based on result status
@@ -1778,41 +1696,27 @@ async def complete_transfer_function_call(tool_call_id: str, request: Request):
event_type = TransferEventType.TRANSFER_TIMEOUT
else:
event_type = TransferEventType.TRANSFER_FAILED
-
- # Create and publish transfer event
- # Add caller_number to result if not already present
- if "caller_number" not in result and caller_number:
- result["caller_number"] = caller_number
-
+
transfer_event = TransferEvent(
type=event_type,
- tool_call_id=tool_call_id,
+ transfer_id=transfer_id,
original_call_sid=original_call_sid or "",
transfer_call_sid=call_sid,
- conference_name=result.get("conference_id"),
+ conference_name=conference_name,
message=result.get("message", ""),
status=result["status"],
action=result.get("action", ""),
reason=result.get("reason"),
- end_call=result.get("end_call", False)
+ end_call=result.get("end_call", False),
)
-
+
# Publish the event via Redis
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 call_transfer_manager.remove_transfer_context(tool_call_id)
-
- logger.info(f"Function call {tool_call_id} completed with result: {result['status']}")
-
+ logger.info(
+ f"Published {event_type} event for {transfer_id} with result: {result['status']}"
+ )
+
except Exception as e:
- logger.error(f"Error completing function call {tool_call_id}: {e}")
-
+ logger.error(f"Error completing transfer {transfer_id}: {e}")
+
return {"status": "completed", "result": result}
-
-
-
-
-
\ No newline at end of file
diff --git a/api/routes/tool.py b/api/routes/tool.py
index b1fdac8..6430b1a 100644
--- a/api/routes/tool.py
+++ b/api/routes/tool.py
@@ -1,8 +1,8 @@
"""API routes for managing tools."""
+import re
from datetime import datetime
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
-import re
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field, field_validator
@@ -70,10 +70,10 @@ class TransferCallConfig(BaseModel):
default=None, description="Custom message to play before transferring the call"
)
timeout: int = Field(
- default=30,
- ge=5,
- le=120,
- description="Maximum time in seconds to wait for destination to answer (5-120 seconds)"
+ default=30,
+ ge=5,
+ le=120,
+ description="Maximum time in seconds to wait for destination to answer (5-120 seconds)",
)
@field_validator("destination")
@@ -83,7 +83,7 @@ class TransferCallConfig(BaseModel):
# Allow empty string for initial creation (like HTTP API tools with empty URL)
if not v.strip():
return v
-
+
# E.164 format: +[1-9]\d{1,14}
e164_pattern = r"^\+[1-9]\d{1,14}$"
if not re.match(e164_pattern, v):
@@ -140,7 +140,9 @@ class CreateToolRequest(BaseModel):
"""Validate that category is a valid ToolCategory value."""
valid_categories = [c.value for c in ToolCategory]
if v not in valid_categories:
- raise ValueError(f"Invalid category '{v}'. Must be one of: {', '.join(valid_categories)}")
+ raise ValueError(
+ f"Invalid category '{v}'. Must be one of: {', '.join(valid_categories)}"
+ )
return v
diff --git a/api/services/configuration/registry.py b/api/services/configuration/registry.py
index c9e3675..ab28a0d 100644
--- a/api/services/configuration/registry.py
+++ b/api/services/configuration/registry.py
@@ -278,7 +278,15 @@ class DograhTTSService(BaseTTSConfiguration):
SARVAM_TTS_MODELS = ["bulbul:v2", "bulbul:v3"]
-SARVAM_V2_VOICES = ["anushka", "manisha", "vidya", "arya", "abhilash", "karun", "hitesh"]
+SARVAM_V2_VOICES = [
+ "anushka",
+ "manisha",
+ "vidya",
+ "arya",
+ "abhilash",
+ "karun",
+ "hitesh",
+]
SARVAM_V3_VOICES = [
"shubh",
"aditya",
diff --git a/api/services/pipecat/event_handlers.py b/api/services/pipecat/event_handlers.py
index aef7a34..b8d4111 100644
--- a/api/services/pipecat/event_handlers.py
+++ b/api/services/pipecat/event_handlers.py
@@ -80,22 +80,14 @@ def register_event_handlers(
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(_transport, _participant):
call_disposed = engine.is_call_disposed()
- transfer_in_progress = getattr(engine, '_transfer_in_progress', False)
-
+
logger.debug(
- f"In on_client_disconnected callback handler. Call disposed: {call_disposed}, "
- f"Transfer in progress: {transfer_in_progress}"
+ f"In on_client_disconnected callback handler. Call disposed: {call_disposed}"
)
# Stop recordings
await audio_buffer.stop_recording()
- # Skip auto hang-up if transfer is in progress
- if transfer_in_progress:
- logger.info("Transfer in progress - skipping auto hang-up, letting redirect handle call")
- return
- logger.info("Transfer in progress - False, proceeding with hang up")
-
await engine.end_call_with_reason(
EndTaskReason.USER_HANGUP.value, abort_immediately=True
)
diff --git a/api/services/pipecat/pipeline_builder.py b/api/services/pipecat/pipeline_builder.py
index 1a94c73..a1f8872 100644
--- a/api/services/pipecat/pipeline_builder.py
+++ b/api/services/pipecat/pipeline_builder.py
@@ -73,8 +73,7 @@ def build_pipeline(
pipeline_engine_callback_processor,
]
)
-
-
+
processors.extend(
[
tts, # TTS
diff --git a/api/services/pipecat/run_pipeline.py b/api/services/pipecat/run_pipeline.py
index 5f5eab8..09d0806 100644
--- a/api/services/pipecat/run_pipeline.py
+++ b/api/services/pipecat/run_pipeline.py
@@ -124,7 +124,7 @@ async def run_pipeline_twilio(
workflow.organization_id,
vad_config,
ambient_noise_config,
- )
+ )
await _run_pipeline(
transport,
workflow_id,
@@ -556,8 +556,9 @@ async def _run_pipeline(
# Create pipeline components
audio_buffer, context = create_pipeline_components(audio_config)
- # Set the context and audio_buffer after creation
+ # Set the context, audio_config, and audio_buffer after creation
engine.set_context(context)
+ engine.set_audio_config(audio_config)
# Set Stasis connection for immediate transfers (if available)
if stasis_connection:
@@ -638,6 +639,9 @@ async def _run_pipeline(
@user_context_aggregator.event_handler("on_user_turn_idle")
async def on_user_turn_idle(aggregator):
+ if engine._transferring_call:
+ logger.debug("Not calling user idle since we are transferring call.")
+ return
await user_idle_handler.handle_idle(aggregator)
@user_context_aggregator.event_handler("on_user_turn_started")
diff --git a/api/services/pipecat/transport_setup.py b/api/services/pipecat/transport_setup.py
index 4882849..6cee7fb 100644
--- a/api/services/pipecat/transport_setup.py
+++ b/api/services/pipecat/transport_setup.py
@@ -24,7 +24,6 @@ from pipecat.transports.websocket.fastapi import (
FastAPIWebsocketParams,
FastAPIWebsocketTransport,
)
-from loguru import logger
librnnoise_path = os.path.normpath(
str(APP_ROOT_DIR / "native" / "rnnoise" / "librnnoise.so")
diff --git a/api/services/telephony/base.py b/api/services/telephony/base.py
index 0e124ad..a154946 100644
--- a/api/services/telephony/base.py
+++ b/api/services/telephony/base.py
@@ -316,16 +316,18 @@ class TelephonyProvider(ABC):
async def transfer_call(
self,
destination: str,
- tool_call_id: str,
+ transfer_id: str,
+ conference_name: str,
timeout: int = 30,
- **kwargs: Any
+ **kwargs: Any,
) -> Dict[str, Any]:
"""
Initiate a call transfer to a destination number.
Args:
destination: The destination phone number (E.164 format)
- tool_call_id: Unique identifier for tracking this transfer
+ transfer_id: Unique identifier for tracking this transfer
+ conference_name: Name of the conference to join the destination into
timeout: Transfer timeout in seconds
**kwargs: Provider-specific additional parameters
@@ -334,7 +336,6 @@ class TelephonyProvider(ABC):
- call_sid: Provider's call identifier
- status: Transfer initiation status
- provider: Provider name
- - webhook_urls: Dict with answer and status callback URLs
Raises:
NotImplementedError: If provider doesn't support transfers
diff --git a/api/services/telephony/call_transfer_manager.py b/api/services/telephony/call_transfer_manager.py
index b542a7f..f843850 100644
--- a/api/services/telephony/call_transfer_manager.py
+++ b/api/services/telephony/call_transfer_manager.py
@@ -5,26 +5,27 @@ Handles transfer event publishing, subscription, and context storage
import asyncio
import time
-from typing import Optional, Dict, Any
-from loguru import logger
+from typing import Dict, Optional
+
import redis.asyncio as aioredis
+from loguru import logger
from api.constants import REDIS_URL
from api.services.telephony.transfer_event_protocol import (
- TransferEvent,
TransferContext,
+ TransferEvent,
TransferEventType,
- TransferRedisChannels
+ TransferRedisChannels,
)
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
self._pubsub_connections: Dict[str, aioredis.client.PubSub] = {}
-
+
async def _get_redis(self) -> aioredis.Redis:
"""Get Redis client instance."""
if not self._redis_client:
@@ -32,34 +33,36 @@ class CallTransferManager:
REDIS_URL, decode_responses=True
)
return self._redis_client
-
- async def store_transfer_context(self, context: TransferContext, ttl: int = 300) -> None:
+
+ async def store_transfer_context(
+ self, context: TransferContext, ttl: int = 300
+ ) -> None:
"""Store transfer context in Redis with TTL.
-
+
Args:
context: Transfer context data
ttl: Time to live in seconds (default 5 minutes)
"""
try:
redis = await self._get_redis()
- key = TransferRedisChannels.transfer_context_key(context.tool_call_id)
+ key = TransferRedisChannels.transfer_context_key(context.transfer_id)
await redis.setex(key, ttl, context.to_json())
- logger.debug(f"Stored transfer context for {context.tool_call_id}")
+ logger.debug(f"Stored transfer context for {context.transfer_id}")
except Exception as e:
logger.error(f"Failed to store transfer context: {e}")
-
- async def get_transfer_context(self, tool_call_id: str) -> Optional[TransferContext]:
+
+ async def get_transfer_context(self, transfer_id: str) -> Optional[TransferContext]:
"""Retrieve transfer context from Redis.
-
+
Args:
- tool_call_id: Tool call identifier
-
+ transfer_id: Transfer identifier
+
Returns:
Transfer context if found, None otherwise
"""
try:
redis = await self._get_redis()
- key = TransferRedisChannels.transfer_context_key(tool_call_id)
+ key = TransferRedisChannels.transfer_context_key(transfer_id)
data = await redis.get(key)
if data:
return TransferContext.from_json(data)
@@ -67,24 +70,24 @@ class CallTransferManager:
except Exception as e:
logger.error(f"Failed to get transfer context: {e}")
return None
-
- async def remove_transfer_context(self, tool_call_id: str) -> None:
+
+ async def remove_transfer_context(self, transfer_id: str) -> None:
"""Remove transfer context from Redis.
-
+
Args:
- tool_call_id: Tool call identifier
+ transfer_id: Transfer identifier
"""
try:
redis = await self._get_redis()
- key = TransferRedisChannels.transfer_context_key(tool_call_id)
+ key = TransferRedisChannels.transfer_context_key(transfer_id)
await redis.delete(key)
- logger.debug(f"Removed transfer context for {tool_call_id}")
+ logger.debug(f"Removed transfer context for {transfer_id}")
except Exception as e:
logger.error(f"Failed to remove transfer context: {e}")
-
+
async def publish_transfer_event(self, event: TransferEvent) -> None:
"""Publish transfer event to Redis channel.
-
+
Args:
event: Transfer event to publish
"""
@@ -92,74 +95,69 @@ class CallTransferManager:
# Add timestamp if not present
if event.timestamp is None:
event.timestamp = time.time()
-
+
redis = await self._get_redis()
- channel = TransferRedisChannels.transfer_events(event.tool_call_id)
+ channel = TransferRedisChannels.transfer_events(event.transfer_id)
await redis.publish(channel, event.to_json())
- logger.info(f"Published {event.type} event for {event.tool_call_id}")
+ logger.info(f"Published {event.type} event for {event.transfer_id}")
except Exception as e:
logger.error(f"Failed to publish transfer event: {e}")
-
+
async def wait_for_transfer_completion(
- self,
- tool_call_id: str,
- timeout_seconds: float = 30.0
+ self, transfer_id: str, timeout_seconds: float = 30.0
) -> Optional[TransferEvent]:
"""Wait for transfer completion event using Redis pub/sub.
-
+
Args:
- tool_call_id: Tool call identifier to wait for
+ transfer_id: Transfer identifier to wait for
timeout_seconds: Maximum time to wait
-
+
Returns:
Transfer completion event if received, None on timeout
"""
- channel = TransferRedisChannels.transfer_events(tool_call_id)
+ channel = TransferRedisChannels.transfer_events(transfer_id)
redis = await self._get_redis()
pubsub = redis.pubsub()
-
+
try:
await pubsub.subscribe(channel)
- logger.info(f"Waiting for transfer completion on {channel} (timeout: {timeout_seconds}s)")
-
+ logger.info(
+ f"Waiting for transfer completion on {channel} (timeout: {timeout_seconds}s)"
+ )
+
# Wait for completion event with timeout
async def wait_for_message():
async for message in pubsub.listen():
if message["type"] == "message":
try:
event = TransferEvent.from_json(message["data"])
- logger.info(f"Received {event.type} event for {tool_call_id}")
-
+ logger.info(
+ f"Received {event.type} event for {transfer_id}"
+ )
+
# Check if this is a completion event
- if event.type in [
- TransferEventType.TRANSFER_ANSWERED, # Call answered = transfer successful
- TransferEventType.TRANSFER_COMPLETED,
- TransferEventType.TRANSFER_FAILED,
- TransferEventType.TRANSFER_CANCELLED,
- TransferEventType.TRANSFER_TIMEOUT
- ]:
+ if (
+ event.type
+ in [
+ TransferEventType.TRANSFER_ANSWERED, # Call answered = transfer successful
+ TransferEventType.TRANSFER_COMPLETED,
+ TransferEventType.TRANSFER_FAILED,
+ TransferEventType.TRANSFER_CANCELLED,
+ TransferEventType.TRANSFER_TIMEOUT,
+ ]
+ ):
return event
except Exception as e:
logger.error(f"Failed to parse transfer event: {e}")
continue
return None
-
+
# Wait with timeout
result = await asyncio.wait_for(wait_for_message(), timeout=timeout_seconds)
return result
-
+
except asyncio.TimeoutError:
- logger.error(f"Transfer completion wait timed out for {tool_call_id}")
- # Publish timeout event for other instances
- timeout_event = TransferEvent(
- type=TransferEventType.TRANSFER_TIMEOUT,
- tool_call_id=tool_call_id,
- original_call_sid="",
- status="failed",
- reason="timeout",
- end_call=True
- )
- await self.publish_transfer_event(timeout_event)
+ logger.debug(f"Transfer completion wait timed out for {transfer_id}")
return None
except Exception as e:
logger.error(f"Error waiting for transfer completion: {e}")
@@ -170,7 +168,7 @@ class CallTransferManager:
await pubsub.close()
except Exception as e:
logger.error(f"Error closing pubsub connection: {e}")
-
+
async def cleanup(self):
"""Clean up Redis connections."""
try:
@@ -181,7 +179,7 @@ class CallTransferManager:
except:
pass
self._pubsub_connections.clear()
-
+
# Close main Redis connection
if self._redis_client:
await self._redis_client.close()
@@ -199,4 +197,4 @@ async def get_call_transfer_manager() -> CallTransferManager:
global _call_transfer_manager
if not _call_transfer_manager:
_call_transfer_manager = CallTransferManager()
- return _call_transfer_manager
\ No newline at end of file
+ return _call_transfer_manager
diff --git a/api/services/telephony/factory.py b/api/services/telephony/factory.py
index 9974484..a79ae1c 100644
--- a/api/services/telephony/factory.py
+++ b/api/services/telephony/factory.py
@@ -128,29 +128,3 @@ async def get_all_telephony_providers() -> List[Type[TelephonyProvider]]:
List of provider classes that can be used for webhook detection
"""
return [CloudonixProvider, TwilioProvider, VobizProvider, VonageProvider]
-
-
-async def get_transfer_provider(organization_id: int) -> TelephonyProvider:
- """
- Get telephony provider that supports call transfers.
-
- Args:
- organization_id: Organization ID for provider lookup
-
- Returns:
- Configured telephony provider that supports transfers
-
- Raises:
- ValueError: If provider doesn't support transfers or org not configured
- """
- provider = await get_telephony_provider(organization_id)
-
- if not provider.supports_transfers():
- config = await load_telephony_config(organization_id)
- provider_type = config.get("provider", "unknown")
- raise ValueError(
- f"Organization telephony provider '{provider_type}' does not support call transfers. "
- f"Only Twilio provider supports transfers."
- )
-
- return provider
diff --git a/api/services/telephony/providers/cloudonix_provider.py b/api/services/telephony/providers/cloudonix_provider.py
index 268afa7..d26849b 100644
--- a/api/services/telephony/providers/cloudonix_provider.py
+++ b/api/services/telephony/providers/cloudonix_provider.py
@@ -686,9 +686,10 @@ class CloudonixProvider(TelephonyProvider):
async def transfer_call(
self,
destination: str,
- tool_call_id: str,
+ transfer_id: str,
+ conference_name: str,
timeout: int = 30,
- **kwargs: Any
+ **kwargs: Any,
) -> Dict[str, Any]:
"""
Cloudonix provider does not support call transfers.
diff --git a/api/services/telephony/providers/twilio_provider.py b/api/services/telephony/providers/twilio_provider.py
index 73fde2a..764227e 100644
--- a/api/services/telephony/providers/twilio_provider.py
+++ b/api/services/telephony/providers/twilio_provider.py
@@ -10,7 +10,6 @@ import aiohttp
from fastapi import HTTPException
from loguru import logger
from twilio.request_validator import RequestValidator
-from pipecat.utils.run_context import set_current_call_sid
from api.enums import WorkflowRunMode
from api.services.telephony.base import (
@@ -283,11 +282,6 @@ class TwilioProvider(TelephonyProvider):
try:
stream_sid = start_msg["start"]["streamSid"]
call_sid = start_msg["start"]["callSid"]
-
- # Set call SID in Pipecat context for use throughout the pipeline
- set_current_call_sid(call_sid)
- logger.info(f"Set call SID context: {call_sid}")
-
except KeyError:
logger.error("Missing streamSid or callSid in start message")
await websocket.close(code=4400, reason="Missing stream identifiers")
@@ -473,16 +467,21 @@ class TwilioProvider(TelephonyProvider):
async def transfer_call(
self,
destination: str,
- tool_call_id: str,
+ transfer_id: str,
+ conference_name: str,
timeout: int = 30,
- **kwargs: Any
+ **kwargs: Any,
) -> Dict[str, Any]:
"""
Initiate a call transfer via Twilio.
+ Uses inline TwiML to put the destination into a conference when they answer,
+ and a status callback to track the transfer outcome.
+
Args:
destination: The destination phone number (E.164 format)
- tool_call_id: Unique identifier for tracking this transfer
+ transfer_id: Unique identifier for tracking this transfer
+ conference_name: Name of the conference to join the destination into
timeout: Transfer timeout in seconds
**kwargs: Additional Twilio-specific parameters
@@ -502,11 +501,18 @@ class TwilioProvider(TelephonyProvider):
backend_endpoint, _ = await get_backend_endpoints()
- # Generate webhook URLs for the transfer call
- call_url = f"{backend_endpoint}/api/v1/telephony/transfer-call-handler/{tool_call_id}"
- status_callback_url = f"{backend_endpoint}/api/v1/telephony/transfer-result/{tool_call_id}"
+ status_callback_url = (
+ f"{backend_endpoint}/api/v1/telephony/transfer-result/{transfer_id}"
+ )
- logger.debug(f"Transfer webhook URLs - Answer: {call_url}, Status: {status_callback_url}")
+ # Inline TwiML: when the destination answers, put them into the conference
+ twiml = f"""
+
+ You have answered a transfer call. Connecting you now.
+
+ {conference_name}
+
+"""
# Prepare Twilio API call data
endpoint = f"{self.base_url}/Calls.json"
@@ -514,49 +520,55 @@ class TwilioProvider(TelephonyProvider):
"To": destination,
"From": from_number,
"Timeout": timeout,
- "Url": call_url,
+ "Twiml": twiml,
"StatusCallback": status_callback_url,
- "StatusCallbackEvent": ["answered", "no-answer", "busy", "failed", "completed"],
- "StatusCallbackMethod": "POST"
+ "StatusCallbackEvent": [
+ "answered",
+ "no-answer",
+ "busy",
+ "failed",
+ "completed",
+ ],
+ "StatusCallbackMethod": "POST",
}
# Add any additional kwargs
data.update(kwargs)
try:
- # Make Twilio API call
- logger.info(f"Making Twilio transfer API call to: {endpoint}")
- logger.info(f"Transfer call data: {data}")
+ logger.debug(f"Transfer call data: {data}")
async with aiohttp.ClientSession() as session:
auth = aiohttp.BasicAuth(self.account_sid, self.auth_token)
async with session.post(endpoint, data=data, auth=auth) as response:
response_status = response.status
response_text = await response.text()
-
- logger.info(f"Twilio transfer API response status: {response_status}")
- logger.info(f"Twilio transfer API response body: {response_text}")
+
+ logger.info(
+ f"Twilio transfer API response status: {response_status}"
+ )
+ logger.debug(f"Twilio transfer API response body: {response_text}")
if response_status in [200, 201]:
try:
response_data = await response.json()
call_sid = response_data.get("sid")
- logger.info(f"Transfer call initiated successfully: {call_sid}")
+ logger.info(
+ f"Transfer call initiated successfully: {call_sid}"
+ )
return {
"call_sid": call_sid,
"status": response_data.get("status", "queued"),
"provider": self.PROVIDER_NAME,
- "webhook_urls": {
- "answer": call_url,
- "status": status_callback_url
- },
"from_number": from_number,
"to_number": destination,
- "raw_response": response_data
+ "raw_response": response_data,
}
except Exception as e:
- logger.error(f"Failed to parse Twilio transfer response JSON: {e}")
+ logger.error(
+ f"Failed to parse Twilio transfer response JSON: {e}"
+ )
raise Exception(f"Failed to parse transfer response: {e}")
else:
error_msg = f"Twilio API call failed with status {response_status}: {response_text}"
diff --git a/api/services/telephony/providers/vobiz_provider.py b/api/services/telephony/providers/vobiz_provider.py
index ae75b6b..7e91bed 100644
--- a/api/services/telephony/providers/vobiz_provider.py
+++ b/api/services/telephony/providers/vobiz_provider.py
@@ -539,9 +539,10 @@ class VobizProvider(TelephonyProvider):
async def transfer_call(
self,
destination: str,
- tool_call_id: str,
+ transfer_id: str,
+ conference_name: str,
timeout: int = 30,
- **kwargs: Any
+ **kwargs: Any,
) -> Dict[str, Any]:
"""
Vobiz provider does not support call transfers.
diff --git a/api/services/telephony/providers/vonage_provider.py b/api/services/telephony/providers/vonage_provider.py
index 5640c41..357d5b4 100644
--- a/api/services/telephony/providers/vonage_provider.py
+++ b/api/services/telephony/providers/vonage_provider.py
@@ -490,9 +490,10 @@ class VonageProvider(TelephonyProvider):
async def transfer_call(
self,
destination: str,
- tool_call_id: str,
+ transfer_id: str,
+ conference_name: str,
timeout: int = 30,
- **kwargs: Any
+ **kwargs: Any,
) -> Dict[str, Any]:
"""
Vonage provider does not support call transfers.
diff --git a/api/services/telephony/transfer_event_protocol.py b/api/services/telephony/transfer_event_protocol.py
index 32056c7..6260676 100644
--- a/api/services/telephony/transfer_event_protocol.py
+++ b/api/services/telephony/transfer_event_protocol.py
@@ -12,7 +12,7 @@ from typing import Any, Dict, Optional
class TransferEventType(str, Enum):
"""Types of transfer events sent between instances."""
-
+
TRANSFER_INITIATED = "transfer_initiated"
TRANSFER_ANSWERED = "transfer_answered"
TRANSFER_COMPLETED = "transfer_completed"
@@ -24,9 +24,9 @@ class TransferEventType(str, Enum):
@dataclass
class TransferEvent:
"""Event data structure for transfer coordination."""
-
+
type: TransferEventType
- tool_call_id: str
+ transfer_id: str
original_call_sid: str
transfer_call_sid: Optional[str] = None
target_number: Optional[str] = None
@@ -37,16 +37,16 @@ class TransferEvent:
reason: Optional[str] = None
end_call: bool = False
timestamp: Optional[float] = None
-
+
def to_json(self) -> str:
"""Convert event to JSON string."""
return json.dumps(asdict(self))
-
- @classmethod
+
+ @classmethod
def from_json(cls, data: str) -> "TransferEvent":
"""Create event from JSON string."""
return cls(**json.loads(data))
-
+
def to_result_dict(self) -> Dict[str, Any]:
"""Convert to function call result format."""
result = {
@@ -56,9 +56,8 @@ class TransferEvent:
"conference_id": self.conference_name,
"transfer_call_sid": self.transfer_call_sid,
"original_call_sid": self.original_call_sid,
- "caller_number": None, # Will be populated by webhook handler
"end_call": self.end_call,
- "reason": self.reason
+ "reason": self.reason,
}
return result
@@ -66,24 +65,24 @@ class TransferEvent:
@dataclass
class TransferContext:
"""Transfer context data stored in Redis."""
-
- tool_call_id: str
+
+ transfer_id: str
call_sid: Optional[str]
target_number: str
tool_uuid: str
original_call_sid: str
- caller_number: Optional[str]
+ conference_name: str
initiated_at: float
-
+
def to_json(self) -> str:
"""Convert context to JSON string."""
return json.dumps(asdict(self))
-
+
@classmethod
def from_json(cls, data: str) -> "TransferContext":
- """Create context from JSON string."""
+ """Create context from JSON string."""
return cls(**json.loads(data))
-
+
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary."""
return asdict(self)
@@ -91,13 +90,13 @@ class TransferContext:
class TransferRedisChannels:
"""Redis channel naming conventions for transfer events."""
-
+
@staticmethod
- def transfer_events(tool_call_id: str) -> str:
- """Channel for transfer events for a specific tool call."""
- return f"transfer:events:{tool_call_id}"
-
+ def transfer_events(transfer_id: str) -> str:
+ """Channel for transfer events for a specific transfer."""
+ return f"transfer:events:{transfer_id}"
+
@staticmethod
- def transfer_context_key(tool_call_id: str) -> str:
+ def transfer_context_key(transfer_id: str) -> str:
"""Redis key for transfer context storage."""
- return f"transfer:context:{tool_call_id}"
\ No newline at end of file
+ return f"transfer:context:{transfer_id}"
diff --git a/api/services/workflow/pipecat_engine.py b/api/services/workflow/pipecat_engine.py
index 99ddc79..8d3e64c 100644
--- a/api/services/workflow/pipecat_engine.py
+++ b/api/services/workflow/pipecat_engine.py
@@ -108,13 +108,16 @@ class PipecatEngine:
# Custom tool manager (initialized in initialize())
self._custom_tool_manager: Optional[CustomToolManager] = None
+ # Tracks whether a call transfer is in progress
+ self._transferring_call: bool = False
+
# Embeddings configuration (passed from run_pipeline.py)
self._embeddings_api_key: Optional[str] = embeddings_api_key
self._embeddings_model: Optional[str] = embeddings_model
self._embeddings_base_url: Optional[str] = embeddings_base_url
-
- # Transfer state tracking - prevents auto hang-up during call transfers
- self._transfer_in_progress: bool = False
+
+ # Audio configuration (set via set_audio_config from _run_pipeline)
+ self._audio_config = None
async def _get_organization_id(self) -> Optional[int]:
"""Get and cache the organization ID from workflow run."""
@@ -248,7 +251,7 @@ class PipecatEngine:
await function_call_params.result_callback(
result, properties=properties
)
-
+
except Exception as e:
logger.error(f"Error in transition function {name}: {str(e)}")
error_result = {"status": "error", "error": str(e)}
@@ -281,7 +284,7 @@ class PipecatEngine:
async def calculate_func(function_call_params: FunctionCallParams) -> None:
logger.info(f"LLM Function Call EXECUTED: safe_calculator")
logger.info(f"Arguments: {function_call_params.arguments}")
-
+
try:
expr = function_call_params.arguments.get("expression", "")
result = safe_calculator(expr)
@@ -297,7 +300,7 @@ class PipecatEngine:
) -> None:
logger.info(f"LLM Function Call EXECUTED: get_current_time")
logger.info(f"Arguments: {function_call_params.arguments}")
-
+
try:
timezone = function_call_params.arguments.get("timezone", "UTC")
result = get_current_time(timezone)
@@ -308,7 +311,7 @@ class PipecatEngine:
async def convert_time_func(function_call_params: FunctionCallParams) -> None:
logger.info(f"LLM Function Call EXECUTED: convert_time")
logger.info(f"Arguments: {function_call_params.arguments}")
-
+
try:
result = convert_time(
function_call_params.arguments.get("source_timezone"),
@@ -339,7 +342,7 @@ class PipecatEngine:
async def retrieve_kb_func(function_call_params: FunctionCallParams) -> None:
logger.info("LLM Function Call EXECUTED: retrieve_from_knowledge_base")
logger.info(f"Arguments: {function_call_params.arguments}")
-
+
try:
query = function_call_params.arguments.get("query", "")
organization_id = await self._get_organization_id()
@@ -540,7 +543,9 @@ class PipecatEngine:
self._current_node, run_in_background=False
)
- frame_to_push = CancelFrame(reason=reason) if abort_immediately else EndFrame(reason=reason)
+ frame_to_push = (
+ CancelFrame(reason=reason) if abort_immediately else EndFrame(reason=reason)
+ )
# Apply disposition mapping - first try call_disposition if it is,
# extracted from the call conversation then fall back to reason
@@ -713,6 +718,10 @@ class PipecatEngine:
f"Stasis connection set for immediate transfers: {connection.channel_id}"
)
+ def set_audio_config(self, audio_config) -> None:
+ """Set the audio configuration for the pipeline."""
+ self._audio_config = audio_config
+
def set_mute_pipeline(self, mute: bool) -> None:
"""Set the pipeline mute state.
@@ -725,6 +734,15 @@ class PipecatEngine:
logger.debug(f"Setting pipeline mute state to: {mute}")
self._mute_pipeline = mute
+ def set_transferring_call(self, transferring: bool) -> None:
+ """Set the call transfer state.
+
+ Args:
+ transferring: True when a call transfer is in progress, False otherwise
+ """
+ logger.debug(f"Setting transferring call state to: {transferring}")
+ self._transferring_call = transferring
+
async def handle_llm_text_frame(self, text: str):
"""Accumulate LLM text frames to build reference text."""
self._current_llm_generation_reference_text += text
diff --git a/api/services/workflow/pipecat_engine_custom_tools.py b/api/services/workflow/pipecat_engine_custom_tools.py
index 2aeb795..fa69d5d 100644
--- a/api/services/workflow/pipecat_engine_custom_tools.py
+++ b/api/services/workflow/pipecat_engine_custom_tools.py
@@ -8,14 +8,17 @@ from __future__ import annotations
import asyncio
import re
+import time
+import uuid
from typing import TYPE_CHECKING, Any, Optional
-import aiohttp
-import httpx
from loguru import logger
from api.db import db_client
from api.enums import ToolCategory
+from api.services.telephony.call_transfer_manager import get_call_transfer_manager
+from api.services.telephony.factory import get_telephony_provider
+from api.services.telephony.transfer_event_protocol import TransferContext
from api.services.workflow.disposition_mapper import (
get_organization_id_from_workflow_run,
)
@@ -24,25 +27,15 @@ from api.services.workflow.tools.custom_tool import (
execute_http_tool,
tool_to_function_schema,
)
+from api.utils.hold_audio import load_hold_audio
from pipecat.adapters.schemas.function_schema import FunctionSchema
from pipecat.frames.frames import (
FunctionCallResultProperties,
- TTSSpeakFrame,
OutputAudioRawFrame,
+ TTSSpeakFrame,
)
from pipecat.services.llm_service import FunctionCallParams
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.call_transfer_manager import get_call_transfer_manager
-from api.services.telephony.transfer_event_protocol import (
- TransferEvent,
- TransferContext,
- TransferEventType,
-)
-
-from dograh.api.utils.common import get_backend_endpoints
if TYPE_CHECKING:
from api.services.workflow.pipecat_engine import PipecatEngine
@@ -134,8 +127,15 @@ class CustomToolManager:
function_name = schema["function"]["name"]
# Create and register the handler
- handler = self._create_handler(tool, function_name)
- self._engine.llm.register_function(function_name, handler)
+ handler, disable_timeout, cancel_on_interruption = self._create_handler(
+ tool, function_name
+ )
+ self._engine.llm.register_function(
+ function_name,
+ handler,
+ cancel_on_interruption=cancel_on_interruption,
+ disable_timeout=disable_timeout,
+ )
logger.debug(
f"Registered custom tool handler: {function_name} "
@@ -155,12 +155,21 @@ class CustomToolManager:
Returns:
Async handler function for the tool
"""
- if tool.category == ToolCategory.END_CALL.value:
- return self._create_end_call_handler(tool, function_name)
- elif tool.category == ToolCategory.TRANSFER_CALL.value:
- return self._create_transfer_call_handler(tool, function_name)
+ # Whether to disable function call timeout
+ disable_timeout = False
+ cancel_on_interruption = True
- return self._create_http_tool_handler(tool, function_name)
+ if tool.category == ToolCategory.END_CALL.value:
+ cancel_on_interruption = False
+ handler = self._create_end_call_handler(tool, function_name)
+ elif tool.category == ToolCategory.TRANSFER_CALL.value:
+ disable_timeout = True
+ cancel_on_interruption = False
+ handler = self._create_transfer_call_handler(tool, function_name)
+ else:
+ handler = self._create_http_tool_handler(tool, function_name)
+
+ return handler, disable_timeout, cancel_on_interruption
def _create_http_tool_handler(self, tool: Any, function_name: str):
"""Create a handler function for an HTTP API tool.
@@ -314,14 +323,6 @@ class CustomToolManager:
logger.info(f"Playing pre-transfer message: {custom_message}")
await self._engine.task.queue_frame(TTSSpeakFrame(custom_message))
- # Get original call information from Pipecat context
- from pipecat.utils.run_context import get_current_call_sid
-
- original_call_sid = get_current_call_sid()
- caller_number = None # TODO: check if this is redundant now
-
- logger.info(f"Found original call context: call_id={original_call_sid}")
-
# Get organization ID for provider configuration
organization_id = await self.get_organization_id()
if not organization_id:
@@ -336,198 +337,143 @@ class CustomToolManager:
validation_error_result, function_call_params, properties
)
return
- #TODO: check if everything in transfer_data is still needed
- # Prepare transfer request data
- transfer_data = {
- "destination": destination,
- "organization_id": organization_id, # Required for provider configuration
- "tool_call_id": function_call_params.tool_call_id, # Use LLM's tool call ID for pipeline coordination
- "tool_uuid": tool.tool_uuid, # Add tool UUID for tracing and validation
- "original_call_sid": original_call_sid, # Original caller's call SID
- "caller_number": caller_number, # Original caller's phone number
- }
+ # 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 = {
+ "status": "failed",
+ "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
+ )
+ return
- import time
+ # Get original callSID from gathered_context
+ workflow_run = await db_client.get_workflow_run_by_id(
+ self._engine._workflow_run_id
+ )
+ original_call_sid = workflow_run.gathered_context.get("call_id")
- # Get backend endpoint URL
- backend_url, _ = await get_backend_endpoints()
+ # Generate a unique transfer ID for tracking this transfer
+ transfer_id = str(uuid.uuid4())
- # Get transfer coordinator for Redis-based coordination
+ # Compute conference name from original call SID
+ conference_name = f"transfer-{original_call_sid}"
+
+ # Mark transfer in progress and mute the pipeline
+ self._engine.set_transferring_call(True)
+ self._engine.set_mute_pipeline(True)
+
+ # Initiate transfer via provider with inline TwiML
+ transfer_result = await provider.transfer_call(
+ destination=destination,
+ transfer_id=transfer_id,
+ conference_name=conference_name,
+ timeout=timeout_seconds,
+ )
+
+ 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(),
+ )
+ await call_transfer_manager.store_transfer_context(transfer_context)
- # Now initiate the transfer call
- transfer_url = f"{backend_url}/api/v1/telephony/call-transfer"
+ # Wait for status callback completion using Redis pub/sub
+ logger.info(
+ f"Transfer call initiated for {destination} (transfer_id={transfer_id}), waiting for completion..."
+ )
- async with httpx.AsyncClient(timeout=30.0) as client:
- response = await client.post(
- transfer_url,
- json=transfer_data,
- headers={"Content-Type": "application/json"},
- # Authentication headers added by provider if needed
+ # Start hold music during transfer waiting period
+ hold_music_stop_event = asyncio.Event()
+ hold_music_task = None
+
+ try:
+ # Use audio config for sample rate (set during pipeline setup)
+ sample_rate = (
+ self._engine._audio_config.transport_out_sample_rate
+ if self._engine._audio_config
+ else 8000
)
- if response.status_code == 200:
- result_data = response.json()
- logger.info(f"Transfer initiated successfully: {result_data}")
+ logger.info(
+ f"Starting hold music at {sample_rate}Hz while waiting for transfer"
+ )
- # Wait for webhook completion using standard Pipecat async pattern
- logger.info(
- f"Transfer call initiated for {destination}, waiting for webhook completion..."
+ # Start hold music as background task
+ hold_music_task = asyncio.create_task(
+ self.play_hold_music_loop(hold_music_stop_event, sample_rate)
+ )
+
+ # Wait for transfer completion using Redis pub/sub
+ logger.info("Waiting for transfer completion via Redis pub/sub...")
+ transfer_event = (
+ await call_transfer_manager.wait_for_transfer_completion(
+ transfer_id, timeout_seconds
)
+ )
- # Start hold music during transfer waiting period
- hold_music_stop_event = asyncio.Event()
- hold_music_task = None
+ except Exception as e:
+ logger.error(f"Error during transfer wait: {e}")
+ transfer_event = None
- try:
- # Mute the pipeline to prevent further LLM generations during transfer
- logger.info("Muting pipeline during transfer call")
- self._engine.set_mute_pipeline(True)
+ finally:
+ # Single cleanup point: stop hold music, unmute pipeline, remove context
+ logger.info(
+ "Transfer wait ended, cleaning up hold music, pipeline state, and transfer context"
+ )
+ hold_music_stop_event.set()
+ if hold_music_task:
+ await hold_music_task
+ self._engine.set_transferring_call(False)
+ self._engine.set_mute_pipeline(False)
+ await call_transfer_manager.remove_transfer_context(transfer_id)
- # Determine sample rate from transport (default to 8000Hz for Twilio)
- sample_rate = 8000
- if hasattr(self._engine.transport, "output") and hasattr(
- self._engine.transport.output(), "sample_rate"
- ):
- sample_rate = getattr(
- self._engine.transport.output(), "sample_rate", 8000
- )
-
- logger.info(
- f"Starting hold music at {sample_rate}Hz while waiting for transfer"
- )
-
- # Start hold music as background task
- hold_music_task = asyncio.create_task(
- self.play_hold_music_loop(
- hold_music_stop_event, sample_rate
- )
- )
-
- # Wait for transfer completion using Redis pub/sub
- logger.info(
- "Waiting for transfer completion via Redis pub/sub..."
- )
- transfer_event = (
- await call_transfer_manager.wait_for_transfer_completion(
- transfer_data["tool_call_id"], timeout_seconds
- )
- )
-
- # Stop hold music and unmute pipeline
- logger.info(
- "Transfer completed, stopping hold music and unmuting pipeline"
- )
- hold_music_stop_event.set()
- if hold_music_task:
- await hold_music_task
- self._engine.set_mute_pipeline(False)
-
- if transfer_event:
- # Get result from transfer event
- final_result = transfer_event.to_result_dict()
-
- # Get transfer context for caller number
- transfer_context = (
- await call_transfer_manager.get_transfer_context(
- transfer_data["tool_call_id"]
- )
- )
- if transfer_context and transfer_context.caller_number:
- final_result["caller_number"] = (
- transfer_context.caller_number
- )
-
- # Handle the transfer result and inform user appropriately
- await self._handle_transfer_result(
- final_result, function_call_params, properties
- )
- else:
- # Handle timeout case
- logger.error(
- f"Transfer call timed out after {timeout_seconds} seconds"
- )
-
- # Create timeout result and handle it through the same flow
- timeout_result = {
- "status": "failed",
- "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
- )
-
- except Exception as e:
- logger.error(f"Error during transfer wait: {e}")
-
- # Stop hold music and unmute pipeline on error
- logger.info(
- "Transfer error, stopping hold music and unmuting pipeline"
- )
- hold_music_stop_event.set()
- if hold_music_task:
- await hold_music_task
- self._engine.set_mute_pipeline(False)
-
- # Handle error case
- error_result = {
- "status": "failed",
- "message": "I'm sorry, but there was an issue processing the transfer. Please try again.",
- "action": "transfer_failed",
- "reason": "system_error",
- "end_call": True,
- }
- await self._handle_transfer_result(
- error_result, function_call_params, properties
- )
-
- else:
- error_data = (
- response.json()
- if response.content
- else {"error": "Unknown error"}
- )
- logger.error(
- f"Transfer initiation failed: {response.status_code} - {error_data}"
- )
-
- # Handle initiation failure with user-friendly message
- initiation_failure_result = {
- "status": "failed",
- "message": "I'm sorry, but I'm having trouble setting up the call transfer right now. There might be a technical issue. Please try again later or contact support.",
- "action": "transfer_failed",
- "reason": "initiation_failed",
- "end_call": True,
- }
-
- await self._handle_transfer_result(
- initiation_failure_result, function_call_params, properties
- )
-
- except httpx.TimeoutException:
- logger.error(f"Transfer call '{function_name}' HTTP request timed out")
-
- # Handle HTTP timeout with user-friendly message
- http_timeout_result = {
- "status": "failed",
- "message": "I'm sorry, but there seems to be a network issue preventing me from setting up the call transfer. Please try again in a moment.",
- "action": "transfer_failed",
- "reason": "network_timeout",
- "end_call": True,
- }
-
- await self._handle_transfer_result(
- http_timeout_result, function_call_params, properties
- )
+ # Handle result (after cleanup)
+ if transfer_event:
+ final_result = transfer_event.to_result_dict()
+ await self._handle_transfer_result(
+ final_result, function_call_params, properties
+ )
+ else:
+ logger.error(
+ f"Transfer call timed out or failed after {timeout_seconds} seconds"
+ )
+ timeout_result = {
+ "status": "failed",
+ "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
+ )
except Exception as e:
logger.error(
f"Transfer call tool '{function_name}' execution failed: {e}"
)
+ self._engine.set_transferring_call(False)
+ self._engine.set_mute_pipeline(False)
# Handle generic exception with user-friendly message
exception_result = {
@@ -550,8 +496,6 @@ class CustomToolManager:
"""Handle different transfer call outcomes and take appropriate action."""
action = result.get("action", "")
status = result.get("status", "")
- message = result.get("message", "")
- should_end_call = result.get("end_call", False)
logger.info(f"Handling transfer result: action={action}, status={status}")
@@ -566,9 +510,7 @@ class CustomToolManager:
)
# Inform LLM of success and end the call with Transfer call reason
- response_properties = FunctionCallResultProperties(
- run_llm=False
- )
+ response_properties = FunctionCallResultProperties(run_llm=False)
await function_call_params.result_callback(
{
"status": "transfer_success",
@@ -585,83 +527,19 @@ class CustomToolManager:
elif action == "transfer_failed":
# Transfer failed - inform user via LLM and then end the call
reason = result.get("reason", "unknown")
- logger.info(f"Transfer failed ({reason}), informing user and ending call")
+ logger.info(f"Transfer failed ({reason}), informing user")
- from pipecat.frames.frames import LLMMessagesAppendFrame
-
- # Create system message with clear instructions for transfer failure
- failure_instruction = {
- "role": "system",
- "content": f"IMPORTANT: The transfer call has FAILED. Reason: {reason}. You must inform the customer about this failure using this message: '{message}' Then immediately say goodbye and end the conversation. Do NOT ask if they need anything else or continue the conversation. Do NOT continue with transfer language.",
- }
-
- # Push the system message to LLM context
- await self._engine.task.queue_frame(
- LLMMessagesAppendFrame([failure_instruction], run_llm=True)
- )
-
- # Also send the function call result for consistency
- response_properties = FunctionCallResultProperties(
- run_llm=False
- ) # LLM will be triggered by system message
await function_call_params.result_callback(
- {"status": "transfer_failed", "reason": reason, "message": message},
- properties=response_properties,
+ {
+ "status": "transfer_failed",
+ "reason": reason,
+ "message": "Transfer failed",
+ }
)
-
- # Set appropriate disposition for analytics
- disposition_map = {
- "no_answer": "transfer_no_answer",
- "busy": "transfer_busy",
- "call_failed": "transfer_failed",
- "timeout": "transfer_timeout",
- "no_destination": "transfer_config_error",
- "invalid_destination": "transfer_config_error",
- "initiation_failed": "transfer_system_error",
- "network_timeout": "transfer_system_error",
- "execution_error": "transfer_system_error",
- }
-
- disposition = disposition_map.get(reason, "transfer_failed")
- logger.info(
- f"Setting disposition: {disposition} for transfer failure reason: {reason}"
- )
-
- # Give the LLM time to speak the message, then end the call with disposition
- # We'll schedule the end call after a brief delay to allow TTS
- logger.info("Scheduling call end after LLM delivers failure message")
-
- import asyncio
-
- # Schedule call end after 3 seconds to allow LLM to speak
- async def delayed_end_call():
- import asyncio
- await asyncio.sleep(3)
- await self._engine.end_call_with_reason(
- f"transfer_failed_{reason}", # Include specific reason in end reason
- abort_immediately=False, # Allow any queued speech to complete
- )
-
- # Create task to end call asynchronously
- asyncio.create_task(delayed_end_call())
-
- elif action == "transfer_completed":
- # This should no longer happen since we ignore "completed" status in webhook
- # to avoid overriding successful transfers
- logger.warning(
- "Received unexpected 'transfer_completed' action - this should be ignored by webhook now"
- )
- logger.warning(
- "If you see this message, there might be an issue with the webhook status filtering"
- )
-
- # For safety, treat it as a generic result without ending the call
- await function_call_params.result_callback(result, properties=properties)
-
else:
# Unknown action, treat as generic success
logger.warning(f"Unknown transfer action: {action}, treating as success")
- await function_call_params.result_callback(result, properties=properties)
+ await function_call_params.result_callback(result)
async def play_hold_music_loop(
self, stop_event: asyncio.Event, sample_rate: int = 8000
diff --git a/api/tasks/function_names.py b/api/tasks/function_names.py
index af92c7c..6d5e73a 100644
--- a/api/tasks/function_names.py
+++ b/api/tasks/function_names.py
@@ -6,4 +6,3 @@ class FunctionNames:
SYNC_CAMPAIGN_SOURCE = "sync_campaign_source"
PROCESS_CAMPAIGN_BATCH = "process_campaign_batch"
PROCESS_KNOWLEDGE_BASE_DOCUMENT = "process_knowledge_base_document"
-
diff --git a/api/utils/hold_audio.py b/api/utils/hold_audio.py
index aa90c6d..e77f24f 100644
--- a/api/utils/hold_audio.py
+++ b/api/utils/hold_audio.py
@@ -5,9 +5,8 @@ This module provides functionality to load hold music audio files at specific sa
with caching to improve performance during multiple calls.
"""
-import io
-import wave
-from typing import Optional, Dict, Tuple
+from typing import Dict, Optional, Tuple
+
import numpy as np
from loguru import logger
@@ -25,46 +24,52 @@ _hold_audio_cache: Dict[Tuple[str, int], np.ndarray] = {}
def load_hold_audio(file_path: str, sample_rate: int) -> Optional[bytes]:
"""Load hold music audio file at the specified sample rate with caching.
-
+
Args:
file_path: Path to the hold music audio file
sample_rate: Target sample rate (8000 or 16000 Hz supported)
-
+
Returns:
Audio data as bytes (PCM16) or None if loading failed
"""
cache_key = (file_path, sample_rate)
-
+
# Check cache first
if cache_key in _hold_audio_cache:
logger.debug(f"Using cached hold audio for {file_path} at {sample_rate}Hz")
audio_data = _hold_audio_cache[cache_key]
return audio_data.tobytes()
-
+
try:
logger.info(f"Loading hold audio from {file_path} at {sample_rate}Hz")
-
+
# Load audio file
sound, file_sample_rate = sf.read(file_path, dtype="int16")
- logger.info(f"Audio file loaded - file sample_rate: {file_sample_rate}, target: {sample_rate}")
-
+ logger.info(
+ f"Audio file loaded - file sample_rate: {file_sample_rate}, target: {sample_rate}"
+ )
+
# Ensure mono audio (take first channel if stereo)
if len(sound.shape) > 1:
sound = sound[:, 0]
-
+
# Resample if needed
if file_sample_rate != sample_rate:
- logger.warning(f"Hold music file has sample rate {file_sample_rate}, expected {sample_rate}")
+ logger.warning(
+ f"Hold music file has sample rate {file_sample_rate}, expected {sample_rate}"
+ )
# For now, we'll use the audio as-is and let the transport handle resampling
# In a production system, you might want to use librosa or scipy for proper resampling
-
+
# Convert to int16 and cache
audio_data = sound.astype(np.int16)
_hold_audio_cache[cache_key] = audio_data
-
- logger.info(f"Hold audio loaded successfully: {len(audio_data)} samples at {sample_rate}Hz")
+
+ logger.info(
+ f"Hold audio loaded successfully: {len(audio_data)} samples at {sample_rate}Hz"
+ )
return audio_data.tobytes()
-
+
except Exception as e:
logger.error(f"Failed to load hold audio file {file_path}: {e}")
return None
@@ -79,11 +84,11 @@ def clear_hold_audio_cache():
def get_cache_info() -> Dict[str, int]:
"""Get information about the current cache state.
-
+
Returns:
Dictionary with cache statistics
"""
return {
"cached_files": len(_hold_audio_cache),
- "total_cache_size": sum(len(data) for data in _hold_audio_cache.values())
- }
\ No newline at end of file
+ "total_cache_size": sum(len(data) for data in _hold_audio_cache.values()),
+ }
diff --git a/pipecat b/pipecat
index 0019ba6..ff7d4c1 160000
--- a/pipecat
+++ b/pipecat
@@ -1 +1 @@
-Subproject commit 0019ba697e4d90dbe70902f1e7df751323960d53
+Subproject commit ff7d4c19bf02bf0e14bfe7ae20e016e04b8ba27d
diff --git a/ui/src/app/tools/[toolUuid]/components/TransferCallToolConfig.tsx b/ui/src/app/tools/[toolUuid]/components/TransferCallToolConfig.tsx
index 613940e..067fad7 100644
--- a/ui/src/app/tools/[toolUuid]/components/TransferCallToolConfig.tsx
+++ b/ui/src/app/tools/[toolUuid]/components/TransferCallToolConfig.tsx
@@ -167,4 +167,4 @@ export function TransferCallToolConfig({
);
-}
\ No newline at end of file
+}
diff --git a/ui/src/app/tools/[toolUuid]/page.tsx b/ui/src/app/tools/[toolUuid]/page.tsx
index 10dda9d..d89d0e5 100644
--- a/ui/src/app/tools/[toolUuid]/page.tsx
+++ b/ui/src/app/tools/[toolUuid]/page.tsx
@@ -410,7 +410,7 @@ const data = await response.json();`;
- {!isEndCallTool && (
+ {!isEndCallTool && !isTransferCallTool && (
)}
-
- {error && (
-
- {error}
-
- )}
-
- {saveSuccess && (
-
- Tool saved successfully!
-
- )}
-
{isEndCallTool ? (
)}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {saveSuccess && (
+
+ Tool saved successfully!
+
+ )}
+
+
+
+
diff --git a/ui/src/app/tools/config.tsx b/ui/src/app/tools/config.tsx
index 5e00a3c..b6c6337 100644
--- a/ui/src/app/tools/config.tsx
+++ b/ui/src/app/tools/config.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Cog, Globe, type LucideIcon, PhoneOff, PhoneForwarded, Puzzle } from "lucide-react";
+import { Cog, Globe, type LucideIcon, PhoneForwarded, PhoneOff, Puzzle } from "lucide-react";
import { type ReactNode } from "react";
export type ToolCategory = "http_api" | "end_call" | "transfer_call" | "native" | "integration";
diff --git a/ui/src/client/client.gen.ts b/ui/src/client/client.gen.ts
index cbfc632..779eafa 100644
--- a/ui/src/client/client.gen.ts
+++ b/ui/src/client/client.gen.ts
@@ -1,8 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts
-import type { ClientOptions } from './types.gen';
-import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch';
+import { type ClientOptions as DefaultClientOptions, type Config, createClient, createConfig } from '@hey-api/client-fetch';
+
import { createClientConfig } from '../lib/apiClient';
+import type { ClientOptions } from './types.gen';
/**
* The `createClientConfig()` function will be called on client initialization
@@ -16,4 +17,4 @@ export type CreateClientConfig =
export const client = createClient(createClientConfig(createConfig({
baseUrl: 'http://127.0.0.1:8000'
-})));
\ No newline at end of file
+})));
diff --git a/ui/src/client/index.ts b/ui/src/client/index.ts
index e64537d..688e3c9 100644
--- a/ui/src/client/index.ts
+++ b/ui/src/client/index.ts
@@ -1,3 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
+export * from './sdk.gen';
export * from './types.gen';
-export * from './sdk.gen';
\ No newline at end of file
diff --git a/ui/src/client/sdk.gen.ts b/ui/src/client/sdk.gen.ts
index 6989399..37f2105 100644
--- a/ui/src/client/sdk.gen.ts
+++ b/ui/src/client/sdk.gen.ts
@@ -1,8 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts
-import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch';
-import type { InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleCloudonixCdrApiV1TelephonyCloudonixCdrPostData, InitiateCallTransferApiV1TelephonyCallTransferPostData, InitiateCallTransferApiV1TelephonyCallTransferPostError, HandleTransferCallAnsweredApiV1TelephonyTransferCallHandlerToolCallIdPostData, HandleTransferCallAnsweredApiV1TelephonyTransferCallHandlerToolCallIdPostError, CompleteTransferFunctionCallApiV1TelephonyTransferResultToolCallIdPostData, CompleteTransferFunctionCallApiV1TelephonyTransferResultToolCallIdPostError, RegisterTransferToolCallApiV1TelephonyRegisterTransferToolCallPostData, ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostResponse, ImpersonateApiV1SuperuserImpersonatePostError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, GetWorkflowCountApiV1WorkflowCountGetData, GetWorkflowCountApiV1WorkflowCountGetResponse, GetWorkflowCountApiV1WorkflowCountGetError, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetResponse, GetAuthUserApiV1UserAuthUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetResponse, GetApiKeysApiV1UserApiKeysGetError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostResponse, CreateApiKeyApiV1UserApiKeysPostError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostResponse, CreateCampaignApiV1CampaignCreatePostError, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetResponse, GetCampaignsApiV1CampaignGetError, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignApiV1CampaignCampaignIdGetError, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostError, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, PauseCampaignApiV1CampaignCampaignIdPausePostError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, ResumeCampaignApiV1CampaignCampaignIdResumePostError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetResponse, ListCredentialsApiV1CredentialsGetError, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostResponse, CreateCredentialApiV1CredentialsPostError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetError, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutError, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetResponse, ListToolsApiV1ToolsGetError, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostResponse, CreateToolApiV1ToolsPostError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteResponse, DeleteToolApiV1ToolsToolUuidDeleteError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetResponse, GetToolApiV1ToolsToolUuidGetError, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateToolApiV1ToolsToolUuidPutError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetResponse, GetIntegrationsApiV1IntegrationGetError, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostResponse, CreateSessionApiV1IntegrationSessionPostError, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetData, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetResponse, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetError, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetResponse, GetSignedUrlApiV1S3SignedUrlGetError, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetResponse, GetFileMetadataApiV1S3FileMetadataGetError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetResponse, GetServiceKeysApiV1UserServiceKeysGetError, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateServiceKeyApiV1UserServiceKeysPostError, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetError, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetTurnCredentialsApiV1TurnCredentialsGetData, GetTurnCredentialsApiV1TurnCredentialsGetResponse, GetTurnCredentialsApiV1TurnCredentialsGetError, OptionsInitApiV1PublicEmbedInitOptionsData, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetData, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetResponse, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetError, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsData, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsError, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1PublicAgentUuidPostError, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SearchChunksApiV1KnowledgeBaseSearchPostError, HealthApiV1HealthGetData, HealthApiV1HealthGetResponse } from './types.gen';
+import type { Client,Options as ClientOptions, TDataShape } from '@hey-api/client-fetch';
+
import { client as _heyApiClient } from './client.gen';
+import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostData, CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetData, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetError, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetData, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetError, GetPublicTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenGetResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetTurnCredentialsApiV1TurnCredentialsGetData, GetTurnCredentialsApiV1TurnCredentialsGetError, GetTurnCredentialsApiV1TurnCredentialsGetResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowCountApiV1WorkflowCountGetData, GetWorkflowCountApiV1WorkflowCountGetError, GetWorkflowCountApiV1WorkflowCountGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixCdrApiV1TelephonyCloudonixCdrPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData, HealthApiV1HealthGetResponse,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, InitiateCallTransferApiV1TelephonyCallTransferPostData, InitiateCallTransferApiV1TelephonyCallTransferPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsData, OptionsTurnCredentialsApiV1PublicEmbedTurnCredentialsSessionTokenOptionsError, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostError, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen';
export type Options = ClientOptions & {
/**
@@ -151,7 +152,11 @@ export const handleCloudonixCdrApiV1TelephonyCloudonixCdrPost = (options: Options) => {
return (options.client ?? _heyApiClient).post({
@@ -164,35 +169,15 @@ export const initiateCallTransferApiV1TelephonyCallTransferPost = (options: Options) => {
- return (options.client ?? _heyApiClient).post({
- url: '/api/v1/telephony/transfer-call-handler/{tool_call_id}',
- ...options
- });
-};
-
/**
* Complete Transfer Function Call
- * Webhook endpoint to complete the function call with transfer result
+ * Webhook endpoint to complete the function call with transfer result.
+ *
+ * Called by Twilio's StatusCallback when the transfer call status changes.
*/
-export const completeTransferFunctionCallApiV1TelephonyTransferResultToolCallIdPost = (options: Options) => {
- return (options.client ?? _heyApiClient).post({
- url: '/api/v1/telephony/transfer-result/{tool_call_id}',
- ...options
- });
-};
-
-/**
- * Register Transfer Tool Call
- * Register a pending transfer function call for webhook completion
- */
-export const registerTransferToolCallApiV1TelephonyRegisterTransferToolCallPost = (options?: Options) => {
- return (options?.client ?? _heyApiClient).post({
- url: '/api/v1/telephony/register-transfer-tool-call',
+export const completeTransferFunctionCallApiV1TelephonyTransferResultTransferIdPost = (options: Options) => {
+ return (options.client ?? _heyApiClient).post({
+ url: '/api/v1/telephony/transfer-result/{transfer_id}',
...options
});
};
@@ -1636,4 +1621,4 @@ export const healthApiV1HealthGet = (optio
url: '/api/v1/health',
...options
});
-};
\ No newline at end of file
+};
diff --git a/ui/src/client/types.gen.ts b/ui/src/client/types.gen.ts
index 3f3ebf9..54e08cd 100644
--- a/ui/src/client/types.gen.ts
+++ b/ui/src/client/types.gen.ts
@@ -882,16 +882,14 @@ export type TransferCallConfig = {
};
/**
- * Request model for initiating call transfer using webhook-driven completion
+ * Request model for initiating a call transfer.
*/
export type TransferCallRequest = {
destination: string;
organization_id: number;
+ transfer_id: string;
+ conference_name: string;
timeout?: number | null;
- tool_call_id?: string | null;
- tool_uuid?: string | null;
- original_call_sid?: string | null;
- caller_number?: string | null;
};
/**
@@ -1611,16 +1609,16 @@ export type InitiateCallTransferApiV1TelephonyCallTransferPostResponses = {
200: unknown;
};
-export type HandleTransferCallAnsweredApiV1TelephonyTransferCallHandlerToolCallIdPostData = {
+export type CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostData = {
body?: never;
path: {
- tool_call_id: string;
+ transfer_id: string;
};
query?: never;
- url: '/api/v1/telephony/transfer-call-handler/{tool_call_id}';
+ url: '/api/v1/telephony/transfer-result/{transfer_id}';
};
-export type HandleTransferCallAnsweredApiV1TelephonyTransferCallHandlerToolCallIdPostErrors = {
+export type CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostErrors = {
/**
* Not found
*/
@@ -1631,59 +1629,9 @@ export type HandleTransferCallAnsweredApiV1TelephonyTransferCallHandlerToolCallI
422: HttpValidationError;
};
-export type HandleTransferCallAnsweredApiV1TelephonyTransferCallHandlerToolCallIdPostError = HandleTransferCallAnsweredApiV1TelephonyTransferCallHandlerToolCallIdPostErrors[keyof HandleTransferCallAnsweredApiV1TelephonyTransferCallHandlerToolCallIdPostErrors];
+export type CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostError = CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostErrors[keyof CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostErrors];
-export type HandleTransferCallAnsweredApiV1TelephonyTransferCallHandlerToolCallIdPostResponses = {
- /**
- * Successful Response
- */
- 200: unknown;
-};
-
-export type CompleteTransferFunctionCallApiV1TelephonyTransferResultToolCallIdPostData = {
- body?: never;
- path: {
- tool_call_id: string;
- };
- query?: never;
- url: '/api/v1/telephony/transfer-result/{tool_call_id}';
-};
-
-export type CompleteTransferFunctionCallApiV1TelephonyTransferResultToolCallIdPostErrors = {
- /**
- * Not found
- */
- 404: unknown;
- /**
- * Validation Error
- */
- 422: HttpValidationError;
-};
-
-export type CompleteTransferFunctionCallApiV1TelephonyTransferResultToolCallIdPostError = CompleteTransferFunctionCallApiV1TelephonyTransferResultToolCallIdPostErrors[keyof CompleteTransferFunctionCallApiV1TelephonyTransferResultToolCallIdPostErrors];
-
-export type CompleteTransferFunctionCallApiV1TelephonyTransferResultToolCallIdPostResponses = {
- /**
- * Successful Response
- */
- 200: unknown;
-};
-
-export type RegisterTransferToolCallApiV1TelephonyRegisterTransferToolCallPostData = {
- body?: never;
- path?: never;
- query?: never;
- url: '/api/v1/telephony/register-transfer-tool-call';
-};
-
-export type RegisterTransferToolCallApiV1TelephonyRegisterTransferToolCallPostErrors = {
- /**
- * Not found
- */
- 404: unknown;
-};
-
-export type RegisterTransferToolCallApiV1TelephonyRegisterTransferToolCallPostResponses = {
+export type CompleteTransferFunctionCallApiV1TelephonyTransferResultTransferIdPostResponses = {
/**
* Successful Response
*/
@@ -4982,4 +4930,4 @@ export type HealthApiV1HealthGetResponse = HealthApiV1HealthGetResponses[keyof H
export type ClientOptions = {
baseUrl: 'http://127.0.0.1:8000' | (string & {});
-};
\ No newline at end of file
+};