diff --git a/.agents/skills/deepgram-python-conversational-stt/SKILL.md b/.agents/skills/deepgram-python-conversational-stt/SKILL.md index 4d9bfac6..d025044d 100644 --- a/.agents/skills/deepgram-python-conversational-stt/SKILL.md +++ b/.agents/skills/deepgram-python-conversational-stt/SKILL.md @@ -91,7 +91,7 @@ with client.listen.v2.connect( **No `language` parameter** on v2 — language is implied by model (`flux-general-en`) or hinted via `language_hint` on multi. -For application-controlled turns, use `eot_threshold="1.0"` with a sufficiently large `eot_timeout_ms`, then call `conn.send_force_end_turn()` for the active turn. ForceEndTurn requires deployment enablement; see `examples/16-transcription-force-end-turn.py`. +For application-controlled turns, use `eot_threshold="1.0"` with a sufficiently large `eot_timeout_ms`, then call `conn.send_force_end_turn()` for the active turn. ForceEndTurn is available on Deepgram-hosted deployments, including EU and AU. Self-hosted availability depends on the deployed release; see `examples/16-transcription-force-end-turn.py`. ## Events (server → client) diff --git a/.fernignore b/.fernignore index 8334454b..c6caf3e2 100644 --- a/.fernignore +++ b/.fernignore @@ -35,15 +35,16 @@ src/deepgram/speak/v2/socket_client.py # The generated eot_threshold descriptions omit that eot_timeout_ms still ends # idle turns. The DeepgramListenProviderV2 model below shares this patch with -# its language_hint compatibility shim. Remove these patches after the API -# definition includes the timeout requirement and Fern regenerates them. +# its language_hint compatibility shim. Remove these patches after +# https://github.com/deepgram/deepgram-docs/pull/1202 is merged and Fern +# regenerates them. # [temporarily frozen] src/deepgram/types/listen_v2eot_threshold.py src/deepgram/requests/deepgram_listen_provider_v2.py # Agent TTS provider expressivity must reject fractional values rather than # allowing Pydantic v1 to truncate them before the API sees the request. -# [temporarily frozen — remove when Fern emits a strict integer] +# [temporarily frozen — remove when Pydantic v1 support is dropped] src/deepgram/types/speak_settings_v1provider.py src/deepgram/types/deepgram.py diff --git a/AGENTS.md b/AGENTS.md index 356dbfb9..37010768 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,11 +62,11 @@ Current temporarily frozen files: - `.gitignore` — Fern generates a baseline version and has regenerated it before (`fdcce88`, `ad93815`, `4bee463`). We hand-add the local coverage artifact ignores (`.coverage`, `htmlcov/`, `coverage.xml`). Before each regen, unfreeze and re-diff so Fern's own additions are picked up, then re-apply the three coverage lines. - `src/deepgram/speak/v1/socket_client.py` — optional message param defaults, broad exception catch - `src/deepgram/speak/v2/socket_client.py` — same (optional `send_flush`/`send_close`/`send_interrupt` defaults, broad exception catch); new websocket TTS client added in the 2026-07-08 regen. `send_interrupt` carries no required payload so it takes the same optional-default treatment as the other control sends; `send_configure` deliberately keeps its required argument (a Configure with no settings is meaningless) -- `src/deepgram/types/listen_v2eot_threshold.py`, `src/deepgram/requests/deepgram_listen_provider_v2.py` — complete the generated `eot_threshold=1.0` description: it suppresses confidence-based detection, while `eot_timeout_ms` still ends idle turns. Unfreeze when the API definition provides that qualifier. -- `src/deepgram/types/speak_settings_v1provider.py`, `src/deepgram/types/deepgram.py` — validate Agent TTS `expressivity` as `pydantic.StrictInt` so Pydantic v1 rejects fractional values instead of truncating them before they reach the API. Regression coverage in `tests/custom/test_socket_client_shims.py`. Unfreeze when Fern emits a strict integer. - `src/deepgram/listen/v1/socket_client.py` — same - `src/deepgram/listen/v2/socket_client.py` — same (broad except, optional `send_close_stream` default). As of the 2026-08-11 regen the generator properly types `send_configure(ListenV2Configure)` and puts `ListenV2ConfigureSuccess` in the response Union, so those are taken from the generator; the only `send_configure` patch retained is runtime tolerance for a raw dict (sent verbatim) for back-compat with pre-typed-model callers - `src/deepgram/agent/v1/socket_client.py` — same + `_sanitize_numeric_types` +- `src/deepgram/types/listen_v2eot_threshold.py`, `src/deepgram/requests/deepgram_listen_provider_v2.py` — complete the generated `eot_threshold=1.0` description: it suppresses confidence-based detection, while `eot_timeout_ms` still ends idle turns. Unfreeze after https://github.com/deepgram/deepgram-docs/pull/1202 is merged and Fern regenerates the descriptions. +- `src/deepgram/types/speak_settings_v1provider.py`, `src/deepgram/types/deepgram.py` — validate Agent TTS `expressivity` as `pydantic.StrictInt` so Pydantic v1 rejects fractional values instead of truncating them before they reach the API. Regression coverage in `tests/custom/test_socket_client_shims.py`. Unfreeze when Pydantic v1 support is dropped. - `src/deepgram/agent/v1/types/agent_v1settings_agent_context.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent.py`, `src/deepgram/agent/v1/types/agent_v1settings.py`, `src/deepgram/agent/v1/requests/agent_v1settings_agent_context.py`, `src/deepgram/agent/v1/requests/agent_v1settings_agent.py`, `src/deepgram/agent/v1/requests/agent_v1settings.py` — backward-compat patches for the 2026-05-05 Agent Settings schema restructure. These preserve callable `AgentV1SettingsAgent(...)`, keep `AgentV1Settings.agent` accepting both that wrapper and `agent_id` strings, restore the legacy request TypedDict shapes, remap legacy `messages=[...]` / nested `context=AgentV1SettingsAgentContext(messages=[...])` usage into the new `context={"messages": [...]}` wire shape, and keep read-side `obj.messages` access working. - `src/deepgram/core/api_error.py`, `src/deepgram/core/parse_error.py` — credential redaction. Every websocket `connect()` path raises `ApiError(headers=dict(headers), ...)` with the full request headers, and both error types stringify that dict, so an unredacted `Authorization` reached `str(e)`, tracebacks, log aggregators and error trackers (which serialise attributes as well as the message). Both now mask credential values at construction via `_secure_logging.redact_sensitive_headers`, preserving non-sensitive headers (`dg-request-id`) for debugging. This is the same threat `_secure_logging.py` covers for the `websockets` DEBUG handshake logs, via the other path to it. Regression coverage in `tests/custom/test_api_error_redaction.py`. Unfreeze if the generator starts redacting credentials itself. - `src/deepgram/core/query_encoder.py` — coerces Python bools to lowercase `"true"`/`"false"` before they reach `urllib.parse.urlencode` (which would otherwise produce `"True"`/`"False"` via `str()` and break websocket query strings). Only the four `*/connect()` paths call `urlencode`; HTTP raw clients hand params to httpx, which lowercases bools itself, so the patch is a no-op for the HTTP path. Once Fern's websocket codegen normalizes bools (or the spec types these as `boolean` end-to-end), this can be unfrozen. diff --git a/examples/16-transcription-force-end-turn.py b/examples/16-transcription-force-end-turn.py index 31df4f6d..6500d031 100644 --- a/examples/16-transcription-force-end-turn.py +++ b/examples/16-transcription-force-end-turn.py @@ -21,9 +21,10 @@ push-to-talk button being released. The connection stays open after a forced end: the turn index advances and transcription continues. -Note: ForceEndTurn requires server-side enablement and is not available on every -deployment. Where it is not enabled the server replies UNPARSABLE_CLIENT_MESSAGE and -closes the connection; this example reports that and exits. +ForceEndTurn is available on Deepgram-hosted deployments, including EU and AU. +Self-hosted availability depends on the deployed release. Unsupported deployments +reply UNPARSABLE_CLIENT_MESSAGE and close the connection; this example reports that +and exits. """ import os diff --git a/examples/32-voice-agent-force-end-turn.py b/examples/32-voice-agent-force-end-turn.py index a5861462..1cdcbd5b 100644 --- a/examples/32-voice-agent-force-end-turn.py +++ b/examples/32-voice-agent-force-end-turn.py @@ -36,6 +36,8 @@ def main() -> None: user_started = threading.Event() agent_finished = threading.Event() + force_end_turn_rejected = threading.Event() + force_end_turn_error: str | None = None settings = AgentV1Settings( audio=AgentV1SettingsAudio(input=AgentV1SettingsAudioInput(encoding="linear16", sample_rate=44100)), @@ -56,6 +58,7 @@ def main() -> None: with DeepgramClient().agent.v1.connect() as agent: def on_message(message: object) -> None: + nonlocal force_end_turn_error message_type = getattr(message, "type", None) if message_type == "UserStartedSpeaking": user_started.set() @@ -67,6 +70,9 @@ def on_message(message: object) -> None: print("AgentAudioDone received") elif message_type in {"Warning", "Error"}: print(f"{message_type}: {message.code} - {message.description}") + if message.code == "FORCE_END_TURN_UNSUPPORTED": + force_end_turn_error = f"{message.code}: {message.description}" + force_end_turn_rejected.set() agent.on(EventType.MESSAGE, on_message) agent.on(EventType.ERROR, lambda error: print(f"Connection error: {error}")) @@ -88,8 +94,14 @@ def on_message(message: object) -> None: print("Sending ForceEndTurn") agent.send_force_end_turn() - if not agent_finished.wait(15): - raise TimeoutError("Timed out waiting for the agent response") + deadline = time.monotonic() + 15 + while not agent_finished.wait(0.1): + if force_end_turn_rejected.is_set(): + raise RuntimeError(f"ForceEndTurn failed: {force_end_turn_error}") + if time.monotonic() >= deadline: + raise TimeoutError("Timed out waiting for the agent response") + if force_end_turn_rejected.is_set(): + raise RuntimeError(f"ForceEndTurn failed: {force_end_turn_error}") if __name__ == "__main__":