-
Notifications
You must be signed in to change notification settings - Fork 125
feat(agent): support multi-provider speak/think configuration and typed listen parameters #676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
9f06d65
78846e8
a51380e
0e2a77a
65cb442
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,33 +6,39 @@ src/deepgram/client.py | |
|
|
||
| # WireMock mappings: removed duplicate empty-body /v1/listen stub that causes | ||
| # non-deterministic matching failures | ||
| wiremock/wiremock-mappings.json | ||
| # [temporarily frozen — .bak preserves our patch during regen] | ||
| wiremock/wiremock-mappings.json.bak | ||
|
|
||
| # Wire test with manual fix: transcribe_file() requires request=bytes parameter | ||
| tests/wire/test_listen_v1_media.py | ||
| # [temporarily frozen — .bak preserves our patch during regen] | ||
| tests/wire/test_listen_v1_media.py.bak | ||
|
|
||
| # WebSocket socket clients: | ||
| # - Optional message parameter defaults for send_flush, send_close, send_clear, | ||
| # send_finalize, send_close_stream, send_keep_alive | ||
| # - construct_type instead of parse_obj_as (skip_validation for unknown WS messages) | ||
| # - except Exception (broad catch for custom transports) | ||
| # - _sanitize_numeric_types in agent socket client (float→int for API) | ||
| src/deepgram/speak/v1/socket_client.py | ||
| src/deepgram/listen/v1/socket_client.py | ||
| src/deepgram/listen/v2/socket_client.py | ||
| src/deepgram/agent/v1/socket_client.py | ||
| # [temporarily frozen — .bak preserves our patches during regen] | ||
| src/deepgram/speak/v1/socket_client.py.bak | ||
| src/deepgram/listen/v1/socket_client.py.bak | ||
| src/deepgram/listen/v2/socket_client.py.bak | ||
| src/deepgram/agent/v1/socket_client.py.bak | ||
|
||
|
|
||
| # Type files with manual int type corrections (Fern generates float for speaker/channel/num_words) | ||
| src/deepgram/types/listen_v1response_results_utterances_item.py | ||
| src/deepgram/types/listen_v1response_results_utterances_item_words_item.py | ||
| src/deepgram/types/listen_v1response_results_channels_item_alternatives_item_paragraphs_paragraphs_item.py | ||
| # [temporarily frozen — .bak preserves our patches during regen] | ||
| src/deepgram/types/listen_v1response_results_utterances_item.py.bak | ||
| src/deepgram/types/listen_v1response_results_utterances_item_words_item.py.bak | ||
| src/deepgram/types/listen_v1response_results_channels_item_alternatives_item_paragraphs_paragraphs_item.py.bak | ||
|
|
||
| # Redact type with Union[str, Sequence[str]] support (Fern narrows to Union[Literal, Any]) | ||
| src/deepgram/types/listen_v1redact.py | ||
| # [temporarily frozen — .bak preserves our patch during regen] | ||
| src/deepgram/types/listen_v1redact.py.bak | ||
|
|
||
| # Listen client files with Union[str, Sequence[str]] array param support | ||
| src/deepgram/listen/v1/client.py | ||
| src/deepgram/listen/v2/client.py | ||
| # [temporarily frozen — .bak preserves our patches during regen] | ||
| src/deepgram/listen/v1/client.py.bak | ||
| src/deepgram/listen/v2/client.py.bak | ||
|
|
||
| # Hand-written custom tests | ||
| tests/custom/test_text_builder.py | ||
|
|
@@ -75,4 +81,4 @@ AGENTS.md | |
| # Folders to ignore | ||
| .github | ||
| docs | ||
| examples | ||
| examples | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,308 @@ | ||
| # This file was auto-generated by Fern from our API Definition. | ||
|
|
||
| import json | ||
| import typing | ||
| from json.decoder import JSONDecodeError | ||
|
|
||
| import websockets | ||
| import websockets.sync.connection as websockets_sync_connection | ||
| from ...core.events import EventEmitterMixin, EventType | ||
| from ...core.unchecked_base_model import construct_type | ||
| from .types.agent_v1agent_audio_done import AgentV1AgentAudioDone | ||
| from .types.agent_v1agent_started_speaking import AgentV1AgentStartedSpeaking | ||
| from .types.agent_v1agent_thinking import AgentV1AgentThinking | ||
| from .types.agent_v1conversation_text import AgentV1ConversationText | ||
| from .types.agent_v1error import AgentV1Error | ||
| from .types.agent_v1function_call_request import AgentV1FunctionCallRequest | ||
| from .types.agent_v1inject_agent_message import AgentV1InjectAgentMessage | ||
| from .types.agent_v1inject_user_message import AgentV1InjectUserMessage | ||
| from .types.agent_v1injection_refused import AgentV1InjectionRefused | ||
| from .types.agent_v1keep_alive import AgentV1KeepAlive | ||
| from .types.agent_v1prompt_updated import AgentV1PromptUpdated | ||
| from .types.agent_v1receive_function_call_response import AgentV1ReceiveFunctionCallResponse | ||
| from .types.agent_v1send_function_call_response import AgentV1SendFunctionCallResponse | ||
| from .types.agent_v1settings import AgentV1Settings | ||
| from .types.agent_v1settings_applied import AgentV1SettingsApplied | ||
| from .types.agent_v1speak_updated import AgentV1SpeakUpdated | ||
| from .types.agent_v1update_prompt import AgentV1UpdatePrompt | ||
| from .types.agent_v1update_speak import AgentV1UpdateSpeak | ||
| from .types.agent_v1user_started_speaking import AgentV1UserStartedSpeaking | ||
| from .types.agent_v1warning import AgentV1Warning | ||
| from .types.agent_v1welcome import AgentV1Welcome | ||
|
|
||
| try: | ||
| from websockets.legacy.client import WebSocketClientProtocol # type: ignore | ||
| except ImportError: | ||
| from websockets import WebSocketClientProtocol # type: ignore | ||
|
|
||
| def _sanitize_numeric_types(obj: typing.Any) -> typing.Any: | ||
| """ | ||
| Recursively convert float values that are whole numbers to int. | ||
|
|
||
| Workaround for Fern-generated models that type integer API fields | ||
| (like sample_rate) as float, causing JSON serialization to produce | ||
| values like 44100.0 instead of 44100. The Deepgram API rejects | ||
| float representations of integer fields. | ||
|
|
||
| See: https://github.com/deepgram/internal-api-specs/issues/205 | ||
| """ | ||
| if isinstance(obj, dict): | ||
| return {k: _sanitize_numeric_types(v) for k, v in obj.items()} | ||
| elif isinstance(obj, list): | ||
| return [_sanitize_numeric_types(item) for item in obj] | ||
| elif isinstance(obj, float) and obj.is_integer(): | ||
| return int(obj) | ||
| return obj | ||
|
|
||
|
|
||
| V1SocketClientResponse = typing.Union[ | ||
| AgentV1ReceiveFunctionCallResponse, | ||
| AgentV1PromptUpdated, | ||
| AgentV1SpeakUpdated, | ||
| AgentV1InjectionRefused, | ||
| AgentV1Welcome, | ||
| AgentV1SettingsApplied, | ||
| AgentV1ConversationText, | ||
| AgentV1UserStartedSpeaking, | ||
| AgentV1AgentThinking, | ||
| AgentV1FunctionCallRequest, | ||
| AgentV1AgentStartedSpeaking, | ||
| AgentV1AgentAudioDone, | ||
| AgentV1Error, | ||
| AgentV1Warning, | ||
| bytes, | ||
| ] | ||
|
|
||
|
|
||
| class AsyncV1SocketClient(EventEmitterMixin): | ||
| def __init__(self, *, websocket: WebSocketClientProtocol): | ||
| super().__init__() | ||
| self._websocket = websocket | ||
|
|
||
| async def __aiter__(self): | ||
| async for message in self._websocket: | ||
| if isinstance(message, bytes): | ||
| yield message | ||
| else: | ||
| yield construct_type(type_=V1SocketClientResponse, object_=json.loads(message)) # type: ignore | ||
|
|
||
| async def start_listening(self): | ||
| """ | ||
| Start listening for messages on the websocket connection. | ||
|
|
||
| Emits events in the following order: | ||
| - EventType.OPEN when connection is established | ||
| - EventType.MESSAGE for each message received | ||
| - EventType.ERROR if an error occurs | ||
| - EventType.CLOSE when connection is closed | ||
| """ | ||
| await self._emit_async(EventType.OPEN, None) | ||
| try: | ||
| async for raw_message in self._websocket: | ||
| if isinstance(raw_message, bytes): | ||
| parsed = raw_message | ||
| else: | ||
| json_data = json.loads(raw_message) | ||
| parsed = construct_type(type_=V1SocketClientResponse, object_=json_data) # type: ignore | ||
| await self._emit_async(EventType.MESSAGE, parsed) | ||
| except Exception as exc: | ||
| await self._emit_async(EventType.ERROR, exc) | ||
| finally: | ||
| await self._emit_async(EventType.CLOSE, None) | ||
|
|
||
| async def send_settings(self, message: AgentV1Settings) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1Settings. | ||
| """ | ||
| await self._send_model(message) | ||
|
|
||
| async def send_update_speak(self, message: AgentV1UpdateSpeak) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1UpdateSpeak. | ||
| """ | ||
| await self._send_model(message) | ||
|
|
||
| async def send_inject_user_message(self, message: AgentV1InjectUserMessage) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1InjectUserMessage. | ||
| """ | ||
| await self._send_model(message) | ||
|
|
||
| async def send_inject_agent_message(self, message: AgentV1InjectAgentMessage) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1InjectAgentMessage. | ||
| """ | ||
| await self._send_model(message) | ||
|
|
||
| async def send_function_call_response(self, message: AgentV1SendFunctionCallResponse) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1SendFunctionCallResponse. | ||
| """ | ||
| await self._send_model(message) | ||
|
|
||
| async def send_keep_alive(self, message: typing.Optional[AgentV1KeepAlive] = None) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1KeepAlive. | ||
| """ | ||
| await self._send_model(message or AgentV1KeepAlive()) | ||
|
|
||
| async def send_update_prompt(self, message: AgentV1UpdatePrompt) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1UpdatePrompt. | ||
| """ | ||
| await self._send_model(message) | ||
|
|
||
| async def send_media(self, message: bytes) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a bytes. | ||
| """ | ||
| await self._send(message) | ||
|
|
||
| async def recv(self) -> V1SocketClientResponse: | ||
| """ | ||
| Receive a message from the websocket connection. | ||
| """ | ||
| data = await self._websocket.recv() | ||
| if isinstance(data, bytes): | ||
| return data # type: ignore | ||
| json_data = json.loads(data) | ||
| return construct_type(type_=V1SocketClientResponse, object_=json_data) # type: ignore | ||
|
|
||
| async def _send(self, data: typing.Any) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| """ | ||
| if isinstance(data, dict): | ||
| data = json.dumps(data) | ||
| await self._websocket.send(data) | ||
|
|
||
| async def _send_model(self, data: typing.Any) -> None: | ||
| """ | ||
| Send a Pydantic model to the websocket connection. | ||
| """ | ||
| await self._send(_sanitize_numeric_types(data.dict())) | ||
|
|
||
|
|
||
| class V1SocketClient(EventEmitterMixin): | ||
| def __init__(self, *, websocket: websockets_sync_connection.Connection): | ||
| super().__init__() | ||
| self._websocket = websocket | ||
|
|
||
| def __iter__(self): | ||
| for message in self._websocket: | ||
| if isinstance(message, bytes): | ||
| yield message | ||
| else: | ||
| yield construct_type(type_=V1SocketClientResponse, object_=json.loads(message)) # type: ignore | ||
|
|
||
| def start_listening(self): | ||
| """ | ||
| Start listening for messages on the websocket connection. | ||
|
|
||
| Emits events in the following order: | ||
| - EventType.OPEN when connection is established | ||
| - EventType.MESSAGE for each message received | ||
| - EventType.ERROR if an error occurs | ||
| - EventType.CLOSE when connection is closed | ||
| """ | ||
| self._emit(EventType.OPEN, None) | ||
| try: | ||
| for raw_message in self._websocket: | ||
| if isinstance(raw_message, bytes): | ||
| parsed = raw_message | ||
| else: | ||
| json_data = json.loads(raw_message) | ||
| parsed = construct_type(type_=V1SocketClientResponse, object_=json_data) # type: ignore | ||
| self._emit(EventType.MESSAGE, parsed) | ||
| except Exception as exc: | ||
| self._emit(EventType.ERROR, exc) | ||
| finally: | ||
| self._emit(EventType.CLOSE, None) | ||
|
|
||
| def send_settings(self, message: AgentV1Settings) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1Settings. | ||
| """ | ||
| self._send_model(message) | ||
|
|
||
| def send_update_speak(self, message: AgentV1UpdateSpeak) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1UpdateSpeak. | ||
| """ | ||
| self._send_model(message) | ||
|
|
||
| def send_inject_user_message(self, message: AgentV1InjectUserMessage) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1InjectUserMessage. | ||
| """ | ||
| self._send_model(message) | ||
|
|
||
| def send_inject_agent_message(self, message: AgentV1InjectAgentMessage) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1InjectAgentMessage. | ||
| """ | ||
| self._send_model(message) | ||
|
|
||
| def send_function_call_response(self, message: AgentV1SendFunctionCallResponse) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1SendFunctionCallResponse. | ||
| """ | ||
| self._send_model(message) | ||
|
|
||
| def send_keep_alive(self, message: typing.Optional[AgentV1KeepAlive] = None) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1KeepAlive. | ||
| """ | ||
| self._send_model(message or AgentV1KeepAlive()) | ||
|
|
||
| def send_update_prompt(self, message: AgentV1UpdatePrompt) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a AgentV1UpdatePrompt. | ||
| """ | ||
| self._send_model(message) | ||
|
|
||
| def send_media(self, message: bytes) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| The message will be sent as a bytes. | ||
| """ | ||
| self._send(message) | ||
|
|
||
| def recv(self) -> V1SocketClientResponse: | ||
| """ | ||
| Receive a message from the websocket connection. | ||
| """ | ||
| data = self._websocket.recv() | ||
| if isinstance(data, bytes): | ||
| return data # type: ignore | ||
| json_data = json.loads(data) | ||
| return construct_type(type_=V1SocketClientResponse, object_=json_data) # type: ignore | ||
|
|
||
| def _send(self, data: typing.Any) -> None: | ||
| """ | ||
| Send a message to the websocket connection. | ||
| """ | ||
| if isinstance(data, dict): | ||
| data = json.dumps(data) | ||
| self._websocket.send(data) | ||
|
|
||
| def _send_model(self, data: typing.Any) -> None: | ||
| """ | ||
| Send a Pydantic model to the websocket connection. | ||
| """ | ||
| self._send(_sanitize_numeric_types(data.dict())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description says this adds support for the new "Think" agent message type, but the only functional change here is swapping
.fernignoreentries to point at.bakbackups (plus adding those backup files). If this PR is just the "prepare repo for regeneration" step, please update the PR description/title accordingly and ensure a follow-up commit in this PR restores.fernignoreto the real paths and removes the.bakfiles before merging; otherwise this PR won’t actually deliver the stated feature support.