From 77042efb128a06fb1674f7ad38523fd572f562be Mon Sep 17 00:00:00 2001 From: Alvaro Navarro Date: Fri, 27 Feb 2026 11:04:51 +0100 Subject: [PATCH 1/4] feat(voice): add wait NCCO action --- voice/src/vonage_voice/models/__init__.py | 13 +++++++++++- voice/src/vonage_voice/models/enums.py | 1 + voice/src/vonage_voice/models/ncco.py | 26 +++++++++++++++++++++++ voice/tests/test_ncco_actions.py | 24 +++++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/voice/src/vonage_voice/models/__init__.py b/voice/src/vonage_voice/models/__init__.py index aeb86a7b..3ad0f38e 100644 --- a/voice/src/vonage_voice/models/__init__.py +++ b/voice/src/vonage_voice/models/__init__.py @@ -15,7 +15,17 @@ TtsLanguageCode, ) from .input_types import Dtmf, Speech -from .ncco import Connect, Conversation, Input, NccoAction, Notify, Record, Stream, Talk +from .ncco import ( + Connect, + Conversation, + Input, + NccoAction, + Notify, + Record, + Stream, + Talk, + Wait, +) from .requests import ( AudioStreamOptions, CreateCallRequest, @@ -63,6 +73,7 @@ 'Speech', 'Stream', 'Talk', + 'Wait', 'ToPhone', 'TtsLanguageCode', 'TtsStreamOptions', diff --git a/voice/src/vonage_voice/models/enums.py b/voice/src/vonage_voice/models/enums.py index faa007ca..98ecae9b 100644 --- a/voice/src/vonage_voice/models/enums.py +++ b/voice/src/vonage_voice/models/enums.py @@ -16,6 +16,7 @@ class NccoActionType(str, Enum): STREAM = 'stream' INPUT = 'input' NOTIFY = 'notify' + WAIT = 'wait' class ConnectEndpointType(str, Enum): diff --git a/voice/src/vonage_voice/models/ncco.py b/voice/src/vonage_voice/models/ncco.py index 72388719..b29b7cb9 100644 --- a/voice/src/vonage_voice/models/ncco.py +++ b/voice/src/vonage_voice/models/ncco.py @@ -267,3 +267,29 @@ class Notify(NccoAction): eventUrl: list[str] eventMethod: Optional[str] = None action: NccoActionType = NccoActionType.NOTIFY + + +class Wait(NccoAction): + """Use the Wait action to add a pause to an NCCO. + + The wait period starts when the action is executed and ends after the provided + or default timeout value. Execution of the NCCO then resumes with the next action. + + Args: + timeout (Optional[float]): Duration of the wait period in seconds. Valid values + are from 0.1 to 7200. Values below 0.1 are treated as 0.1; values above + 7200 are treated as 7200. If not specified, defaults to 10 seconds. + """ + + timeout: Optional[float] = 10.0 + action: NccoActionType = NccoActionType.WAIT + + @model_validator(mode='after') + def clamp_timeout(self): + if self.timeout is None: + self.timeout = 10.0 + elif self.timeout < 0.1: + self.timeout = 0.1 + elif self.timeout > 7200: + self.timeout = 7200.0 + return self diff --git a/voice/tests/test_ncco_actions.py b/voice/tests/test_ncco_actions.py index 4e1a3c75..d05f1ca1 100644 --- a/voice/tests/test_ncco_actions.py +++ b/voice/tests/test_ncco_actions.py @@ -325,3 +325,27 @@ def test_notify_options(): 'eventMethod': 'POST', 'action': 'notify', } + + +def test_wait_default_timeout(): + wait = ncco.Wait() + assert wait.model_dump(by_alias=True, exclude_none=True) == { + 'timeout': 10.0, + 'action': 'wait', + } + + +def test_wait_custom_timeout(): + wait = ncco.Wait(timeout=0.5) + assert wait.model_dump(by_alias=True, exclude_none=True) == { + 'timeout': 0.5, + 'action': 'wait', + } + + +def test_wait_timeout_clamped_min_max(): + wait_min = ncco.Wait(timeout=0.01) + assert wait_min.timeout == 0.1 + + wait_max = ncco.Wait(timeout=10000) + assert wait_max.timeout == 7200.0 From 425bcd201a8a36a4ae645529bb5a437e2eeb3775 Mon Sep 17 00:00:00 2001 From: Alvaro Navarro Date: Fri, 27 Feb 2026 11:17:00 +0100 Subject: [PATCH 2/4] feat(voice): add tranfer NCCO action --- voice/src/vonage_voice/models/__init__.py | 13 ++------ voice/src/vonage_voice/models/enums.py | 1 + voice/src/vonage_voice/models/ncco.py | 37 +++++++++++++++++++++++ voice/tests/test_ncco_actions.py | 35 +++++++++++++++++++++ 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/voice/src/vonage_voice/models/__init__.py b/voice/src/vonage_voice/models/__init__.py index 3ad0f38e..ef045321 100644 --- a/voice/src/vonage_voice/models/__init__.py +++ b/voice/src/vonage_voice/models/__init__.py @@ -15,17 +15,7 @@ TtsLanguageCode, ) from .input_types import Dtmf, Speech -from .ncco import ( - Connect, - Conversation, - Input, - NccoAction, - Notify, - Record, - Stream, - Talk, - Wait, -) +from .ncco import Connect, Conversation, Input, NccoAction, Notify, Record, Stream, Talk, Transfer, Wait from .requests import ( AudioStreamOptions, CreateCallRequest, @@ -73,6 +63,7 @@ 'Speech', 'Stream', 'Talk', + 'Transfer', 'Wait', 'ToPhone', 'TtsLanguageCode', diff --git a/voice/src/vonage_voice/models/enums.py b/voice/src/vonage_voice/models/enums.py index 98ecae9b..868abce4 100644 --- a/voice/src/vonage_voice/models/enums.py +++ b/voice/src/vonage_voice/models/enums.py @@ -17,6 +17,7 @@ class NccoActionType(str, Enum): INPUT = 'input' NOTIFY = 'notify' WAIT = 'wait' + TRANSFER = 'transfer' class ConnectEndpointType(str, Enum): diff --git a/voice/src/vonage_voice/models/ncco.py b/voice/src/vonage_voice/models/ncco.py index b29b7cb9..e82bb084 100644 --- a/voice/src/vonage_voice/models/ncco.py +++ b/voice/src/vonage_voice/models/ncco.py @@ -293,3 +293,40 @@ def clamp_timeout(self): elif self.timeout > 7200: self.timeout = 7200.0 return self + + +class Transfer(NccoAction): + """Use the Transfer action to move all legs from the current conversation into + another existing conversation. + + The transfer action is synchronous and terminal for the current conversation. + The target conversation's NCCO continues to control its behaviour. + + Args: + conversationId (str): The target conversation ID. + canHear (Optional[list[str]]): Leg UUIDs this participant can hear. If not + provided, the participant can hear everyone. If an empty list is provided, + the participant will not hear any other participants. + canSpeak (Optional[list[str]]): Leg UUIDs this participant can be heard by. If + not provided, the participant can be heard by everyone. If an empty list is + provided, the participant will not be heard by anyone. + mute (Optional[bool]): Set to `True` to mute the participant. When using + `canSpeak`, the `mute` parameter is not supported. + + Raises: + NccoActionError: If the `mute` option is used with the `canSpeak` option. + """ + + conversationId: str + canHear: Optional[list[str]] = None + canSpeak: Optional[list[str]] = None + mute: Optional[bool] = None + action: NccoActionType = NccoActionType.TRANSFER + + @model_validator(mode='after') + def validate_mute_and_can_speak(self): + if self.canSpeak and self.mute: + raise NccoActionError( + 'Cannot use mute option if canSpeak option is specified.' + ) + return self diff --git a/voice/tests/test_ncco_actions.py b/voice/tests/test_ncco_actions.py index d05f1ca1..73764591 100644 --- a/voice/tests/test_ncco_actions.py +++ b/voice/tests/test_ncco_actions.py @@ -349,3 +349,38 @@ def test_wait_timeout_clamped_min_max(): wait_max = ncco.Wait(timeout=10000) assert wait_max.timeout == 7200.0 + + +def test_transfer_basic(): + transfer = ncco.Transfer(conversationId='CON-1234567890') + assert transfer.model_dump(by_alias=True, exclude_none=True) == { + 'conversationId': 'CON-1234567890', + 'action': 'transfer', + } + + +def test_transfer_options(): + transfer = ncco.Transfer( + conversationId='CON-1234567890', + canHear=['leg-a'], + canSpeak=['leg-b', 'leg-c'], + mute=False, + ) + assert transfer.model_dump(by_alias=True, exclude_none=True) == { + 'conversationId': 'CON-1234567890', + 'canHear': ['leg-a'], + 'canSpeak': ['leg-b', 'leg-c'], + 'mute': False, + 'action': 'transfer', + } + + +def test_transfer_mute_with_canspeak_error(): + with raises(NccoActionError) as e: + ncco.Transfer( + conversationId='CON-1234567890', + canSpeak=['leg-a'], + mute=True, + ) + + assert e.match('Cannot use mute option if canSpeak option is specified.') From b189b683a443a73831a8c44ed76ba1f6791caf66 Mon Sep 17 00:00:00 2001 From: Alvaro Navarro Date: Fri, 27 Feb 2026 11:40:13 +0100 Subject: [PATCH 3/4] fix: linter --- voice/src/vonage_voice/models/__init__.py | 91 +++++++++++++---------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/voice/src/vonage_voice/models/__init__.py b/voice/src/vonage_voice/models/__init__.py index ef045321..df81bace 100644 --- a/voice/src/vonage_voice/models/__init__.py +++ b/voice/src/vonage_voice/models/__init__.py @@ -15,7 +15,18 @@ TtsLanguageCode, ) from .input_types import Dtmf, Speech -from .ncco import Connect, Conversation, Input, NccoAction, Notify, Record, Stream, Talk, Transfer, Wait +from .ncco import ( + Connect, + Conversation, + Input, + NccoAction, + Notify, + Record, + Stream, + Talk, + Transfer, + Wait, +) from .requests import ( AudioStreamOptions, CreateCallRequest, @@ -33,43 +44,43 @@ ) __all__ = [ - 'AdvancedMachineDetection', - 'AppEndpoint', - 'AudioStreamOptions', - 'CallInfo', - 'CallList', - 'CallMessage', - 'CallState', - 'Channel', - 'Connect', - 'ConnectEndpointType', - 'Conversation', - 'CreateCallRequest', - 'CreateCallResponse', - 'Dtmf', - 'Embedded', - 'Input', - 'ListCallsFilter', - 'HalLinks', - 'NccoAction', - 'NccoActionType', - 'Notify', - 'OnAnswer', - 'Phone', - 'PhoneEndpoint', - 'Record', - 'Sip', - 'SipEndpoint', - 'Speech', - 'Stream', - 'Talk', - 'Transfer', - 'Wait', - 'ToPhone', - 'TtsLanguageCode', - 'TtsStreamOptions', - 'Vbc', - 'VbcEndpoint', - 'Websocket', - 'WebsocketEndpoint', + "AdvancedMachineDetection", + "AppEndpoint", + "AudioStreamOptions", + "CallInfo", + "CallList", + "CallMessage", + "CallState", + "Channel", + "Connect", + "ConnectEndpointType", + "Conversation", + "CreateCallRequest", + "CreateCallResponse", + "Dtmf", + "Embedded", + "Input", + "ListCallsFilter", + "HalLinks", + "NccoAction", + "NccoActionType", + "Notify", + "OnAnswer", + "Phone", + "PhoneEndpoint", + "Record", + "Sip", + "SipEndpoint", + "Speech", + "Stream", + "Talk", + "Transfer", + "Wait", + "ToPhone", + "TtsLanguageCode", + "TtsStreamOptions", + "Vbc", + "VbcEndpoint", + "Websocket", + "WebsocketEndpoint", ] From 2f79dbf60ce4e4c547c34c1fc83827a02e2be1a6 Mon Sep 17 00:00:00 2001 From: Alvaro Navarro Date: Fri, 27 Feb 2026 17:16:59 +0100 Subject: [PATCH 4/4] fix: linter --- voice/src/vonage_voice/models/ncco.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice/src/vonage_voice/models/ncco.py b/voice/src/vonage_voice/models/ncco.py index e82bb084..6309dc4c 100644 --- a/voice/src/vonage_voice/models/ncco.py +++ b/voice/src/vonage_voice/models/ncco.py @@ -296,8 +296,8 @@ def clamp_timeout(self): class Transfer(NccoAction): - """Use the Transfer action to move all legs from the current conversation into - another existing conversation. + """Use the Transfer action to move all legs from the current conversation into another + existing conversation. The transfer action is synchronous and terminal for the current conversation. The target conversation's NCCO continues to control its behaviour.