Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Mar 26, 2026

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 .fernignore entries to point at .bak backups (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 .fernignore to the real paths and removes the .bak files before merging; otherwise this PR won’t actually deliver the stated feature support.

Copilot uses AI. Check for mistakes.

# 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
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These newly added src/deepgram/**.py.bak files live inside the published deepgram package directory (see pyproject.toml package include config). With no packaging excludes configured, they are likely to be shipped in the wheel/sdist. If these backups are intended to be temporary for regeneration, consider keeping them outside src/ or adding an explicit packaging exclude for *.bak, and make sure they’re removed before merging/releasing.

Copilot uses AI. Check for mistakes.

# 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
Expand Down Expand Up @@ -75,4 +81,4 @@ AGENTS.md
# Folders to ignore
.github
docs
examples
examples
308 changes: 308 additions & 0 deletions src/deepgram/agent/v1/socket_client.py.bak
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()))
Loading
Loading