From 1973bbcc6fb5c111c991c2b807ec3573e77ec516 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Thu, 29 May 2025 12:30:27 -0700 Subject: [PATCH 1/6] feat: support arbitrary attributes for speak provider --- deepgram/__init__.py | 1 - deepgram/client.py | 1 - deepgram/clients/__init__.py | 1 - deepgram/clients/agent/__init__.py | 1 - deepgram/clients/agent/client.py | 2 - deepgram/clients/agent/v1/__init__.py | 1 - .../clients/agent/v1/websocket/__init__.py | 1 - .../clients/agent/v1/websocket/options.py | 65 +++--------- examples/agent/arbitrary_keys/main.py | 100 ++++++++++++++++++ 9 files changed, 115 insertions(+), 58 deletions(-) create mode 100644 examples/agent/arbitrary_keys/main.py diff --git a/deepgram/__init__.py b/deepgram/__init__.py index 95f056d3..b6f2d35c 100644 --- a/deepgram/__init__.py +++ b/deepgram/__init__.py @@ -342,7 +342,6 @@ Listen, ListenProvider, Speak, - SpeakProvider, Header, Item, Properties, diff --git a/deepgram/client.py b/deepgram/client.py index c9d20fd1..4be5e7ac 100644 --- a/deepgram/client.py +++ b/deepgram/client.py @@ -356,7 +356,6 @@ Listen, ListenProvider, Speak, - SpeakProvider, Header, Item, Properties, diff --git a/deepgram/clients/__init__.py b/deepgram/clients/__init__.py index 0f573d2e..19cb924c 100644 --- a/deepgram/clients/__init__.py +++ b/deepgram/clients/__init__.py @@ -365,7 +365,6 @@ Listen, ListenProvider, Speak, - SpeakProvider, Header, Item, Properties, diff --git a/deepgram/clients/agent/__init__.py b/deepgram/clients/agent/__init__.py index 63c598ef..f9ef73d7 100644 --- a/deepgram/clients/agent/__init__.py +++ b/deepgram/clients/agent/__init__.py @@ -40,7 +40,6 @@ Listen, ListenProvider, Speak, - SpeakProvider, Header, Item, Properties, diff --git a/deepgram/clients/agent/client.py b/deepgram/clients/agent/client.py index 6dbfd08f..6eacb7a0 100644 --- a/deepgram/clients/agent/client.py +++ b/deepgram/clients/agent/client.py @@ -39,7 +39,6 @@ Listen as LatestListen, ListenProvider as LatestListenProvider, Speak as LatestSpeak, - SpeakProvider as LatestSpeakProvider, Header as LatestHeader, Item as LatestItem, Properties as LatestProperties, @@ -87,7 +86,6 @@ Listen = LatestListen ListenProvider = LatestListenProvider Speak = LatestSpeak -SpeakProvider = LatestSpeakProvider Header = LatestHeader Item = LatestItem Properties = LatestProperties diff --git a/deepgram/clients/agent/v1/__init__.py b/deepgram/clients/agent/v1/__init__.py index 8d48b80b..5713808b 100644 --- a/deepgram/clients/agent/v1/__init__.py +++ b/deepgram/clients/agent/v1/__init__.py @@ -44,7 +44,6 @@ Listen, ListenProvider, Speak, - SpeakProvider, Header, Item, Properties, diff --git a/deepgram/clients/agent/v1/websocket/__init__.py b/deepgram/clients/agent/v1/websocket/__init__.py index b1cec3f2..c663552d 100644 --- a/deepgram/clients/agent/v1/websocket/__init__.py +++ b/deepgram/clients/agent/v1/websocket/__init__.py @@ -35,7 +35,6 @@ Listen, ListenProvider, Speak, - SpeakProvider, Header, Item, Properties, diff --git a/deepgram/clients/agent/v1/websocket/options.py b/deepgram/clients/agent/v1/websocket/options.py index 8ecea60c..aaaaa394 100644 --- a/deepgram/clients/agent/v1/websocket/options.py +++ b/deepgram/clients/agent/v1/websocket/options.py @@ -2,7 +2,7 @@ # Use of this source code is governed by a MIT license that can be found in the LICENSE file. # SPDX-License-Identifier: MIT -from typing import List, Optional, Union, Any, Tuple +from typing import List, Optional, Union, Any, Tuple, Dict import logging from dataclasses import dataclass, field @@ -167,52 +167,6 @@ class ThinkProvider(BaseResponse): ) -@dataclass -class SpeakProvider(BaseResponse): - """ - This class defines the provider for the Speak model. - """ - - type: Optional[str] = field(default="deepgram") - """ - Deepgram OR OpenAI model to use. - """ - model: Optional[str] = field( - default="aura-2-thalia-en", - metadata=dataclass_config(exclude=lambda f: f is None), - ) - """ - ElevenLabs or Cartesia model to use. - """ - model_id: Optional[str] = field( - default=None, metadata=dataclass_config(exclude=lambda f: f is None) - ) - """ - Cartesia voice configuration. - """ - voice: Optional[CartesiaVoice] = field( - default=None, metadata=dataclass_config(exclude=lambda f: f is None) - ) - """ - Cartesia language. - """ - language: Optional[str] = field( - default=None, metadata=dataclass_config(exclude=lambda f: f is None) - ) - """ - ElevenLabs language. - """ - language_code: Optional[str] = field( - default=None, metadata=dataclass_config(exclude=lambda f: f is None) - ) - - def __getitem__(self, key): - _dict = self.to_dict() - if "voice" in _dict and isinstance(_dict["voice"], dict): - _dict["voice"] = CartesiaVoice.from_dict(_dict["voice"]) - return _dict[key] - - @dataclass class Think(BaseResponse): """ @@ -264,15 +218,26 @@ class Speak(BaseResponse): This class defines any configuration settings for the Speak model. """ - provider: SpeakProvider = field(default_factory=SpeakProvider) + provider: dict = field(default_factory=dict) endpoint: Optional[Endpoint] = field( default=None, metadata=dataclass_config(exclude=lambda f: f is None) ) + def __post_init__(self): + # Allow attribute-style access to provider dict + class AttrDict(dict): + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError(name) + def __setattr__(self, name, value): + self[name] = value + if not isinstance(self.provider, AttrDict): + self.provider = AttrDict(self.provider) + def __getitem__(self, key): _dict = self.to_dict() - if "provider" in _dict and isinstance(_dict["provider"], dict): - _dict["provider"] = SpeakProvider.from_dict(_dict["provider"]) if "endpoint" in _dict and isinstance(_dict["endpoint"], dict): _dict["endpoint"] = Endpoint.from_dict(_dict["endpoint"]) return _dict[key] diff --git a/examples/agent/arbitrary_keys/main.py b/examples/agent/arbitrary_keys/main.py new file mode 100644 index 00000000..428677c3 --- /dev/null +++ b/examples/agent/arbitrary_keys/main.py @@ -0,0 +1,100 @@ +# Copyright 2025 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +# Import dependencies and set up the main function +import requests +import wave +import io +import time +import os +import json +import threading +from datetime import datetime + +from deepgram import ( + DeepgramClient, + DeepgramClientOptions, + AgentWebSocketEvents, + AgentKeepAlive, +) +from deepgram.clients.agent.v1.websocket.options import SettingsOptions + +def main(): + try: + # Initialize the Voice Agent + api_key = os.getenv("DEEPGRAM_API_KEY") + if not api_key: + raise ValueError("DEEPGRAM_API_KEY environment variable is not set") + print(f"API Key found:") + + # Initialize Deepgram client + config = DeepgramClientOptions( + options={ + "keepalive": "true", + # "speaker_playback": "true", + }, + ) + deepgram = DeepgramClient(api_key, config) + connection = deepgram.agent.websocket.v("1") + print("Created WebSocket connection...") + + # 4. Configure the Agent + options = SettingsOptions() + # Audio input configuration + options.audio.input.encoding = "linear16" + options.audio.input.sample_rate = 24000 + # Audio output configuration + options.audio.output.encoding = "linear16" + options.audio.output.sample_rate = 24000 + options.audio.output.container = "wav" + # Agent configuration + options.agent.language = "en" + options.agent.listen.provider.type = "deepgram" + options.agent.listen.provider.model = "nova-3" + options.agent.think.provider.type = "open_ai" + options.agent.think.provider.model = "gpt-4o-mini" + options.agent.think.prompt = "You are a friendly AI assistant." + options.agent.speak.provider.type = "deepgram" + options.agent.speak.provider.model = "aura-2-thalia-en" + options.agent.greeting = "Hello! How can I help you today?" + options.agent.speak.provider.arbitrary_key = "test" + + def on_welcome(self, welcome, **kwargs): + print(f"Welcome message received: {welcome}") + with open("chatlog.txt", 'a') as chatlog: + chatlog.write(f"Welcome message: {welcome}\n") + + def on_settings_applied(self, settings_applied, **kwargs): + print(f"Settings applied: {settings_applied}") + with open("chatlog.txt", 'a') as chatlog: + chatlog.write(f"Settings applied: {settings_applied}\n") + + def on_error(self, error, **kwargs): + print(f"Error received: {error}") + with open("chatlog.txt", 'a') as chatlog: + chatlog.write(f"Error: {error}\n") + + # Register handlers + connection.on(AgentWebSocketEvents.Welcome, on_welcome) + connection.on(AgentWebSocketEvents.SettingsApplied, on_settings_applied) + connection.on(AgentWebSocketEvents.Error, on_error) + print("Event handlers registered") + + # Start the connection + print("Starting WebSocket connection...") + print(options) + if not connection.start(options): + print("Failed to start connection") + return + print("WebSocket connection started successfully") + + # Cleanup + connection.finish() + print("Finished") + + except Exception as e: + print(f"Error: {str(e)}") + +if __name__ == "__main__": + main() From 4cf0542775ee33ac2ffeb4b9f81674a10859f571 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Thu, 29 May 2025 12:33:23 -0700 Subject: [PATCH 2/6] fix: shut up linter --- deepgram/clients/agent/v1/websocket/options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deepgram/clients/agent/v1/websocket/options.py b/deepgram/clients/agent/v1/websocket/options.py index aaaaa394..b1142cd0 100644 --- a/deepgram/clients/agent/v1/websocket/options.py +++ b/deepgram/clients/agent/v1/websocket/options.py @@ -225,11 +225,13 @@ class Speak(BaseResponse): def __post_init__(self): # Allow attribute-style access to provider dict + # pylint: disable=missing-class-docstring class AttrDict(dict): def __getattr__(self, name): try: return self[name] except KeyError: + # pylint: disable=raise-missing-from raise AttributeError(name) def __setattr__(self, name, value): self[name] = value From 2c92d676a8611e997c2abde05943df092644f7ea Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Sat, 7 Jun 2025 14:04:26 -0700 Subject: [PATCH 3/6] feat: make all providers generic --- deepgram/__init__.py | 3 +- deepgram/client.py | 3 +- deepgram/clients/__init__.py | 3 +- deepgram/clients/agent/__init__.py | 3 +- deepgram/clients/agent/client.py | 6 +- deepgram/clients/agent/v1/__init__.py | 3 +- .../clients/agent/v1/websocket/__init__.py | 3 +- .../clients/agent/v1/websocket/options.py | 89 +++++-------------- 8 files changed, 31 insertions(+), 82 deletions(-) diff --git a/deepgram/__init__.py b/deepgram/__init__.py index b6f2d35c..42e6d558 100644 --- a/deepgram/__init__.py +++ b/deepgram/__init__.py @@ -340,7 +340,6 @@ AgentKeepAlive, # sub level Listen, - ListenProvider, Speak, Header, Item, @@ -348,7 +347,7 @@ Parameters, Function, Think, - ThinkProvider, + Provider, Agent, Input, Output, diff --git a/deepgram/client.py b/deepgram/client.py index 4be5e7ac..80c5d9c3 100644 --- a/deepgram/client.py +++ b/deepgram/client.py @@ -354,7 +354,6 @@ AgentKeepAlive, # sub level Listen, - ListenProvider, Speak, Header, Item, @@ -362,7 +361,7 @@ Parameters, Function, Think, - ThinkProvider, + Provider, Agent, Input, Output, diff --git a/deepgram/clients/__init__.py b/deepgram/clients/__init__.py index 19cb924c..c1bb5c5c 100644 --- a/deepgram/clients/__init__.py +++ b/deepgram/clients/__init__.py @@ -363,7 +363,6 @@ AgentKeepAlive, # sub level Listen, - ListenProvider, Speak, Header, Item, @@ -371,7 +370,7 @@ Parameters, Function, Think, - ThinkProvider, + Provider, Agent, Input, Output, diff --git a/deepgram/clients/agent/__init__.py b/deepgram/clients/agent/__init__.py index f9ef73d7..befd2ef8 100644 --- a/deepgram/clients/agent/__init__.py +++ b/deepgram/clients/agent/__init__.py @@ -38,7 +38,6 @@ AgentKeepAlive, # sub level Listen, - ListenProvider, Speak, Header, Item, @@ -46,7 +45,7 @@ Parameters, Function, Think, - ThinkProvider, + Provider, Agent, Input, Output, diff --git a/deepgram/clients/agent/client.py b/deepgram/clients/agent/client.py index 6eacb7a0..e7c8eba9 100644 --- a/deepgram/clients/agent/client.py +++ b/deepgram/clients/agent/client.py @@ -37,7 +37,6 @@ AgentKeepAlive as LatestAgentKeepAlive, # sub level Listen as LatestListen, - ListenProvider as LatestListenProvider, Speak as LatestSpeak, Header as LatestHeader, Item as LatestItem, @@ -45,7 +44,7 @@ Parameters as LatestParameters, Function as LatestFunction, Think as LatestThink, - ThinkProvider as LatestThinkProvider, + Provider as LatestProvider, Agent as LatestAgent, Input as LatestInput, Output as LatestOutput, @@ -84,7 +83,6 @@ AgentKeepAlive = LatestAgentKeepAlive Listen = LatestListen -ListenProvider = LatestListenProvider Speak = LatestSpeak Header = LatestHeader Item = LatestItem @@ -92,7 +90,7 @@ Parameters = LatestParameters Function = LatestFunction Think = LatestThink -ThinkProvider = LatestThinkProvider +Provider = LatestProvider Agent = LatestAgent Input = LatestInput Output = LatestOutput diff --git a/deepgram/clients/agent/v1/__init__.py b/deepgram/clients/agent/v1/__init__.py index 5713808b..cd115c9f 100644 --- a/deepgram/clients/agent/v1/__init__.py +++ b/deepgram/clients/agent/v1/__init__.py @@ -42,7 +42,6 @@ AgentKeepAlive, # sub level Listen, - ListenProvider, Speak, Header, Item, @@ -50,7 +49,7 @@ Parameters, Function, Think, - ThinkProvider, + Provider, Agent, Input, Output, diff --git a/deepgram/clients/agent/v1/websocket/__init__.py b/deepgram/clients/agent/v1/websocket/__init__.py index c663552d..f32c1bb7 100644 --- a/deepgram/clients/agent/v1/websocket/__init__.py +++ b/deepgram/clients/agent/v1/websocket/__init__.py @@ -33,7 +33,6 @@ AgentKeepAlive, # sub level Listen, - ListenProvider, Speak, Header, Item, @@ -41,7 +40,7 @@ Parameters, Function, Think, - ThinkProvider, + Provider, Agent, Input, Output, diff --git a/deepgram/clients/agent/v1/websocket/options.py b/deepgram/clients/agent/v1/websocket/options.py index b1142cd0..bca74cb2 100644 --- a/deepgram/clients/agent/v1/websocket/options.py +++ b/deepgram/clients/agent/v1/websocket/options.py @@ -68,6 +68,16 @@ def __getitem__(self, key): _dict["properties"] = _dict["properties"].copy() return _dict[key] +class Provider(dict): + def __getattr__(self, name): + try: + return self[name] + except KeyError: + # pylint: disable=raise-missing-from + raise AttributeError(name) + def __setattr__(self, name, value): + self[name] = value + @dataclass class Endpoint(BaseResponse): @@ -121,59 +131,13 @@ def __getitem__(self, key): return _dict[key] -@dataclass -class CartesiaVoice(BaseResponse): - """ - This class defines the voice for the Cartesia model. - """ - - mode: str = field( - default="", metadata=dataclass_config(exclude=lambda f: f is None or f == "") - ) - id: str = field( - default="", metadata=dataclass_config(exclude=lambda f: f is None or f == "") - ) - - -@dataclass -class ListenProvider(BaseResponse): - """ - This class defines the provider for the Listen model. - """ - - type: str = field(default="") - model: str = field(default="") - keyterms: Optional[List[str]] = field( - default=None, metadata=dataclass_config(exclude=lambda f: f is None) - ) - - def __getitem__(self, key): - _dict = self.to_dict() - if "keyterms" in _dict and isinstance(_dict["keyterms"], list): - _dict["keyterms"] = [str(keyterm) for keyterm in _dict["keyterms"]] - return _dict[key] - - -@dataclass -class ThinkProvider(BaseResponse): - """ - This class defines the provider for the Think model. - """ - - type: Optional[str] = field(default=None) - model: Optional[str] = field(default=None) - temperature: Optional[float] = field( - default=None, metadata=dataclass_config(exclude=lambda f: f is None) - ) - - @dataclass class Think(BaseResponse): """ This class defines any configuration settings for the Think model. """ - provider: ThinkProvider = field(default_factory=ThinkProvider) + provider: Provider = field(default_factory=Provider) functions: Optional[List[Function]] = field( default=None, metadata=dataclass_config(exclude=lambda f: f is None) ) @@ -184,10 +148,12 @@ class Think(BaseResponse): default=None, metadata=dataclass_config(exclude=lambda f: f is None) ) + def __post_init__(self): + if not isinstance(self.provider, Provider): + self.provider = Provider(self.provider) + def __getitem__(self, key): _dict = self.to_dict() - if "provider" in _dict and isinstance(_dict["provider"], dict): - _dict["provider"] = ThinkProvider.from_dict(_dict["provider"]) if "functions" in _dict and isinstance(_dict["functions"], list): _dict["functions"] = [ Function.from_dict(function) for function in _dict["functions"] @@ -203,12 +169,14 @@ class Listen(BaseResponse): This class defines any configuration settings for the Listen model. """ - provider: ListenProvider = field(default_factory=ListenProvider) + provider: Provider = field(default_factory=Provider) + + def __post_init__(self): + if not isinstance(self.provider, Provider): + self.provider = Provider(self.provider) def __getitem__(self, key): _dict = self.to_dict() - if "provider" in _dict and isinstance(_dict["provider"], dict): - _dict["provider"] = ListenProvider.from_dict(_dict["provider"]) return _dict[key] @@ -218,25 +186,14 @@ class Speak(BaseResponse): This class defines any configuration settings for the Speak model. """ - provider: dict = field(default_factory=dict) + provider: Provider = field(default_factory=Provider) endpoint: Optional[Endpoint] = field( default=None, metadata=dataclass_config(exclude=lambda f: f is None) ) def __post_init__(self): - # Allow attribute-style access to provider dict - # pylint: disable=missing-class-docstring - class AttrDict(dict): - def __getattr__(self, name): - try: - return self[name] - except KeyError: - # pylint: disable=raise-missing-from - raise AttributeError(name) - def __setattr__(self, name, value): - self[name] = value - if not isinstance(self.provider, AttrDict): - self.provider = AttrDict(self.provider) + if not isinstance(self.provider, Provider): + self.provider = Provider(self.provider) def __getitem__(self, key): _dict = self.to_dict() From 4ab6cba0a5bdb604a5db56d45aab2dfb8b711f73 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Sat, 7 Jun 2025 14:07:31 -0700 Subject: [PATCH 4/6] fix: remove the keyterms check --- deepgram/clients/agent/v1/websocket/async_client.py | 3 --- deepgram/clients/agent/v1/websocket/client.py | 8 -------- 2 files changed, 11 deletions(-) diff --git a/deepgram/clients/agent/v1/websocket/async_client.py b/deepgram/clients/agent/v1/websocket/async_client.py index 46524468..ae4822a5 100644 --- a/deepgram/clients/agent/v1/websocket/async_client.py +++ b/deepgram/clients/agent/v1/websocket/async_client.py @@ -224,9 +224,6 @@ async def start( else: raise DeepgramError("Invalid options type") - if self._settings.agent.listen.provider.keyterms is not None and self._settings.agent.listen.provider.model is not None and not self._settings.agent.listen.provider.model.startswith("nova-3"): - raise DeepgramError("Keyterms are only supported for nova-3 models") - try: # speaker substitutes the listening thread if self._speaker is not None: diff --git a/deepgram/clients/agent/v1/websocket/client.py b/deepgram/clients/agent/v1/websocket/client.py index fae7830b..8f7e2322 100644 --- a/deepgram/clients/agent/v1/websocket/client.py +++ b/deepgram/clients/agent/v1/websocket/client.py @@ -225,14 +225,6 @@ def start( else: raise DeepgramError("Invalid options type") - if ( - self._settings.agent.listen.provider - and self._settings.agent.listen.provider.keyterms is not None - and self._settings.agent.listen.provider.model is not None - and not self._settings.agent.listen.provider.model.startswith("nova-3") - ): - raise DeepgramError("Keyterms are only supported for nova-3 models") - try: # speaker substitutes the listening thread if self._speaker is not None: From 998f0ed8fe9a4d9f47e8c9d23108aa2d38389643 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Sat, 7 Jun 2025 14:32:03 -0700 Subject: [PATCH 5/6] chore: docs stirng --- deepgram/clients/agent/v1/websocket/options.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deepgram/clients/agent/v1/websocket/options.py b/deepgram/clients/agent/v1/websocket/options.py index 412bb38d..ffdaa5c1 100644 --- a/deepgram/clients/agent/v1/websocket/options.py +++ b/deepgram/clients/agent/v1/websocket/options.py @@ -69,6 +69,9 @@ def __getitem__(self, key): return _dict[key] class Provider(dict): + """ + Generic attribute class for provider objects. + """ def __getattr__(self, name): try: return self[name] From bf825dcdc9fbca37fe6a089475dfb40e24a65be5 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Mon, 9 Jun 2025 14:13:52 -0700 Subject: [PATCH 6/6] chore: mention that example should fail --- examples/agent/arbitrary_keys/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/agent/arbitrary_keys/main.py b/examples/agent/arbitrary_keys/main.py index 428677c3..14350f43 100644 --- a/examples/agent/arbitrary_keys/main.py +++ b/examples/agent/arbitrary_keys/main.py @@ -2,6 +2,8 @@ # Use of this source code is governed by a MIT license that can be found in the LICENSE file. # SPDX-License-Identifier: MIT +# This example should fail, due to the arbitrary key being included in the settings payload. + # Import dependencies and set up the main function import requests import wave @@ -91,7 +93,8 @@ def on_error(self, error, **kwargs): # Cleanup connection.finish() - print("Finished") + print("Finished! You should see an error for the arbitrary key - scroll up and you can see it is included in the settings payload.") + print("If you do not see that error, this example has failed.") except Exception as e: print(f"Error: {str(e)}")