Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .fern/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"skip_validation": true
}
},
"originGitCommit": "3063c73a3a1dabef36c44211e69ea5e5c1f2b8f0",
"originGitCommit": "3dc7e52769409adb2a41e006ee1fc80fdf642a20",
"originGitCommitIsDirty": true,
"invokedBy": "manual",
"sdkVersion": "7.15.0"
Expand Down
1 change: 1 addition & 0 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ src/deepgram/listen/v2/types/listen_v2warning.py
# Hand-written custom tests
tests/custom/test_agent_custom_think_provider.py
tests/custom/test_agent_history.py
tests/custom/test_agent_think_live.py
tests/custom/test_agent_update_listen.py
tests/custom/test_api_error_redaction.py
tests/custom/test_api_key_env_resolution.py
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Current permanently frozen files:
- `src/deepgram/listen/v2/types/listen_v2close_stream_type.py` — hand-written shim recreating `ListenV2CloseStreamType`, which Fern removed in the 2026-06-15 regen (docs #946). The original generated type wrongly allowed `Union[Literal["Finalize","CloseStream","KeepAlive"], Any]` (v2 copied v1's control-message enum); a CloseStream message's `type` can only ever be `"CloseStream"`. Recreated as the corrected `Literal["CloseStream"]` to preserve the public import path without resurrecting the invalid values. Re-exported from the three `listen` `__init__.py` files (temporarily frozen, below).
- `src/deepgram/transport_interface.py`, `src/deepgram/transport.py`, `src/deepgram/transports/` — custom transport layer. `_TARGET_MODULES` in `transport.py` must include every generated `client.py` and `raw_client.py` that uses the patchable WebSocket connector symbols; update it whenever a regen adds a WebSocket surface. `tests/custom/test_transport.py` enforces this so `transport_factory` cannot silently fall back to Deepgram Cloud.
- `tests/custom/test_agent_custom_think_provider.py` — hand-written regression coverage for custom Think-provider request serialization and response parsing on both synchronous and asynchronous Agent V1 websocket clients
- `tests/custom/test_agent_think_live.py` — hand-written regression coverage for Think Live settings, updates, dynamic injection, and event parsing on both synchronous and asynchronous Agent V1 websocket clients
- `tests/custom/test_agent_history.py` — hand-written regression test for Agent History websocket payload parsing
- `tests/custom/test_api_key_env_resolution.py` — hand-written regression test that `DEEPGRAM_API_KEY` is resolved when the client is constructed rather than when the module is imported. The generated base client takes `os.getenv(...)` as a default argument, which Python evaluates once at import, so `load_dotenv()` placed below the imports left the default captured as `None` (issue #734). `client.py` re-reads the variable; this pins that, plus explicit-key and `access_token` precedence and the no-key-anywhere error
- `tests/custom/test_compat_aliases.py` — hand-written regression test for backward-compatible alias imports after regen renames
Expand Down Expand Up @@ -78,7 +79,7 @@ Current temporarily frozen files:
- `src/deepgram/listen/v2/types/listen_v2connected.py`, `src/deepgram/listen/v2/types/listen_v2turn_info.py`, `src/deepgram/listen/v2/types/listen_v2turn_info_words_item.py`, `src/deepgram/listen/v2/types/listen_v2configure_success.py`, `src/deepgram/listen/v2/types/listen_v2configure_success_thresholds.py`, `src/deepgram/listen/v2/types/listen_v2configure_failure.py`, `src/deepgram/listen/v2/types/listen_v2fatal_error.py`, `src/deepgram/listen/v2/types/listen_v2warning.py` — read-side compatibility for the SDK 7.7 Listen V2 response retype. Through 7.6, `V2SocketClientResponse` contained `typing.Any`, so every response was returned as a raw dict; fixing the union made responses typed models and broke callers using `response["field"]`. These generated response classes inherit the hand-written base above, preserving read-only wire-key subscript access alongside canonical attribute access. Restore direct `UncheckedBaseModel` inheritance and unfreeze these files in the next major release.
- `src/deepgram/types/deepgram_listen_provider_v2.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent_listen_provider.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent_context_listen_provider.py` — behavioural back-compat shim for the `language_hint` -> `language_hints` rename (2026-06-15 regen). The public field was historically (incorrectly) singular and accepted a str or a list; the API field is `language_hints` (a list, and the server uses `deny_unknown_fields` so the singular key is rejected on the wire). Each carries a hand-added `model_validator(mode='before')` / `root_validator(pre=True)` that remaps a legacy `language_hint=` kwarg and drops the dead singular key. Remove and unfreeze the language alias when it is retired in a future major.
- `src/deepgram/types/shared_topics.py`, `src/deepgram/types/shared_intents.py`, `src/deepgram/requests/shared_topics.py`, `src/deepgram/requests/shared_intents.py`, and the generated root/type/request barrels — direct Topics and Intents segments plus their generated exports remain Fern-owned, but carry the v7 deprecated `results` construction facade and Params shape for the hand-written wrappers above. The models normalize legacy `results.{topics,intents}.segments` input to direct `segments` while preserving direct fields and deprecated read facades. Freeze these generated files during the parent reconciliation; retain all direct generated segments and exports alongside the compatibility surface.
- `src/deepgram/core/client_wrapper.py` — Fern's `runtime_version: true` wrapper correctly uses installed package metadata, but its `PackageNotFoundError` fallback must remain the latest published version for source checkouts, vendored copies, zipapps, and frozen bundles. The fallback carries `# x-release-please-version` and `.github/release-please-config.json` includes the file as a generic extra file, so Release Please updates it with `pyproject.toml` and `.fern/metadata.json`. Unfreeze when Fern emits a release-managed fallback.
- `src/deepgram/core/client_wrapper.py` — Fern's `runtime_version: true` wrapper correctly uses installed package metadata, but its `PackageNotFoundError` fallback must remain the latest published version for source checkouts, vendored copies, zipapps, and frozen bundles. The retained `7.15.0` fallback carries `# x-release-please-version`, and `.github/release-please-config.json` includes the file as a generic extra file so Release Please updates it with `pyproject.toml` and `.fern/metadata.json`. Unfreeze when Fern emits a release-managed fallback.
- `src/deepgram/speak/v2/types/speak_v2warning.py`, `src/deepgram/speak/v2/requests/speak_v2warning.py` — whitespace cleanup for generated Warning docstrings. Unfreeze when Fern emits whitespace-clean output.
- `src/deepgram/agent/v1/types/agent_v1update_listen_listen.py` — backward-compat patch for the 2026-07-31 `AgentV1UpdateListen` provider retype. The `provider` field changed from a bare `DeepgramListenProviderV2` to the required discriminated union `AgentV1UpdateListenListenProvider` (`_V1`/`_V2`, discriminant `version`). Carries a hand-added `model_validator(mode='before')` / `root_validator(pre=True)` that coerces a legacy `DeepgramListenProviderV1`/`V2` (or a dict lacking the `version` discriminant) into the new shape so existing callers keep working. Remove and unfreeze when the old provider payloads are retired in a future major. NOTE: this patch was silently lost once (it was absent from `.fernignore`, so a regen overwrote it) — keep it frozen.
- `tests/wire/test_manage_v1_projects_keys.py` — restored wire coverage for the legacy `CreateKeyV1RequestOneParams` request alias so future regens do not silently drop that compatibility check
Expand Down
3 changes: 3 additions & 0 deletions examples/30-voice-agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ def on_message(message: AgentV1SocketClientResponse) -> None:
role = getattr(message, "role", "unknown")
content = getattr(message, "content", "")
print(f"[{role}] {content}")
conversational_behavior = getattr(message, "conversational_behavior", None)
if conversational_behavior is not None:
print(f" conversational_behavior: {conversational_behavior}")
if role == "assistant":
assistant_response_event.set()
elif msg_type == "UserStartedSpeaking":
Expand Down
135 changes: 135 additions & 0 deletions examples/33-voice-agent-think-live.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""
Example: Voice Agent with Think Live (Agent V1)

This deterministic text-only example configures normal Think alongside Think Live,
then injects a user message and queues a Think Live-generated dynamic message after
SettingsApplied. It does not write or play audio. Frustration detection and filler
responses are model- and timing-dependent, so they may not appear on every run.
"""

import threading
import time

from dotenv import load_dotenv

from deepgram import DeepgramClient
from deepgram.agent.v1.types import (
AgentV1InjectDynamicAgentMessage,
AgentV1InjectUserMessage,
AgentV1Settings,
AgentV1SettingsAgent,
AgentV1SettingsAgentListen,
AgentV1SettingsAgentListenProvider_V1,
AgentV1SettingsAudio,
AgentV1SettingsAudioInput,
AgentV1UpdateThinkLive,
)
from deepgram.core.events import EventType
from deepgram.types.deepgram_think_live_provider_v1 import DeepgramThinkLiveProviderV1
from deepgram.types.speak_settings_v1 import SpeakSettingsV1
from deepgram.types.speak_settings_v1provider import SpeakSettingsV1Provider_Deepgram
from deepgram.types.think_live_settings_v1 import ThinkLiveSettingsV1
from deepgram.types.think_settings_v1 import ThinkSettingsV1
from deepgram.types.think_settings_v1provider import ThinkSettingsV1Provider_OpenAi

USER_MESSAGE = "Please tell me one short fact about the first all-female spacewalk."

load_dotenv()
client = DeepgramClient()


def think_live_settings() -> ThinkLiveSettingsV1:
return ThinkLiveSettingsV1(
provider=DeepgramThinkLiveProviderV1(
agent_backchannel_sensitivity=0.4,
presence_check_max_count=1,
presence_check_interval_ms=5000,
frustration_max_count=1,
)
)


try:
with client.agent.v1.connect() as agent:
settings_applied = threading.Event()
think_live_updated = threading.Event()
assistant_response = threading.Event()

settings = AgentV1Settings(
audio=AgentV1SettingsAudio(
input=AgentV1SettingsAudioInput(encoding="linear16", sample_rate=24000)
),
agent=AgentV1SettingsAgent(
listen=AgentV1SettingsAgentListen(
provider=AgentV1SettingsAgentListenProvider_V1(type="deepgram", model="nova-3")
),
think=ThinkSettingsV1(
provider=ThinkSettingsV1Provider_OpenAi(type="open_ai", model="gpt-4o-mini"),
prompt="You are helpful and concise.",
),
think_live=think_live_settings(),
speak=SpeakSettingsV1(
provider=SpeakSettingsV1Provider_Deepgram(type="deepgram", model="aura-2-asteria-en")
),
),
)

def on_message(message: object) -> None:
if isinstance(message, bytes):
print(f"Received agent audio ({len(message)} bytes; not saved)")
return

msg_type = getattr(message, "type", "Unknown")
if msg_type == "SettingsApplied":
print("Received SettingsApplied event")
settings_applied.set()
elif msg_type == "ThinkLiveUpdated":
print("Received ThinkLiveUpdated event")
think_live_updated.set()
elif msg_type == "FrustrationDetected":
print(
"Received FrustrationDetected event "
f"(count={getattr(message, 'count', 'unknown')}, "
f"escalated={getattr(message, 'escalated', 'unknown')})"
)
elif msg_type == "ConversationText":
role = getattr(message, "role", "unknown")
print(f"[{role}] {getattr(message, 'content', '')}")
conversational_behavior = getattr(message, "conversational_behavior", None)
if conversational_behavior is not None:
print(f" conversational_behavior: {conversational_behavior}")
if role == "assistant" and conversational_behavior in (None, "default"):
assistant_response.set()
elif msg_type == "Error":
print("Agent reported an error")
else:
print(f"Received {msg_type} event")

agent.on(EventType.OPEN, lambda _: print("Connection opened"))
agent.on(EventType.MESSAGE, on_message)
agent.on(EventType.CLOSE, lambda _: print("Connection closed"))
agent.on(EventType.ERROR, lambda _: print("Connection error"))

listener = threading.Thread(target=agent.start_listening, daemon=True)
listener.start()

agent.send_settings(settings)
if not settings_applied.wait(10):
raise TimeoutError("Timed out waiting for settings to apply")

# This update demonstrates the ThinkLiveUpdated event without changing behavior.
agent.send_update_think_live(AgentV1UpdateThinkLive(think_live=think_live_settings()))
if not think_live_updated.wait(10):
raise TimeoutError("Timed out waiting for Think Live update to apply")

agent.send_inject_user_message(AgentV1InjectUserMessage(content=USER_MESSAGE))
agent.send_inject_dynamic_agent_message(AgentV1InjectDynamicAgentMessage(behavior="queue"))

if not assistant_response.wait(30):
raise TimeoutError("Timed out waiting for an assistant response")

# Bound the final callbacks without writing any audio data to disk.
time.sleep(1)

except Exception as error:
print(f"Voice Agent example stopped: {type(error).__name__}")
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ This directory contains comprehensive examples demonstrating how to use the Deep
- **30-voice-agent.py** - Voice Agent configuration and usage
- **31-voice-agent-session-recording.py** - Record selected Voice Agent events as JSON
- **32-voice-agent-force-end-turn.py** - End an active Voice Agent Flux turn with ForceEndTurn
- **33-voice-agent-think-live.py** - Configure Think Live, update it in-session, and observe dynamic Agent events

### 40-49: Text Intelligence (Read)

Expand Down
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion src/deepgram/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
DeepgramListenProviderV1,
DeepgramListenProviderV2,
DeepgramModel,
DeepgramThinkLiveProviderV1,
DeleteAgentConfigurationV1Response,
DeleteAgentVariableV1Response,
DeleteProjectInviteV1Response,
Expand Down Expand Up @@ -232,6 +233,7 @@
SpeakV2Speed,
SpeakV2SpeedValue,
SpeakV2Tag,
ThinkLiveSettingsV1,
ThinkSettingsV1,
ThinkSettingsV1ContextLength,
ThinkSettingsV1Endpoint,
Expand Down Expand Up @@ -290,6 +292,7 @@
DeepgramListenProviderV1Params,
DeepgramListenProviderV2Params,
DeepgramParams,
DeepgramThinkLiveProviderV1Params,
DeleteProjectInviteV1ResponseParams,
DeleteProjectKeyV1ResponseParams,
DeleteProjectMemberV1ResponseParams,
Expand Down Expand Up @@ -406,6 +409,7 @@
SpeakSettingsV1Provider_ElevenLabsParams,
SpeakSettingsV1Provider_OpenAiParams,
SpeakV2AcceptedResponseParams,
ThinkLiveSettingsV1Params,
ThinkSettingsV1ContextLengthParams,
ThinkSettingsV1EndpointParams,
ThinkSettingsV1FunctionsItemEndpointParams,
Expand Down Expand Up @@ -505,6 +509,8 @@
"DeepgramListenProviderV2Params": ".requests",
"DeepgramModel": ".types",
"DeepgramParams": ".requests",
"DeepgramThinkLiveProviderV1": ".types",
"DeepgramThinkLiveProviderV1Params": ".requests",
"DefaultAioHttpClient": "._default_clients",
"DefaultAsyncHttpxClient": "._default_clients",
"DeleteAgentConfigurationV1Response": ".types",
Expand Down Expand Up @@ -807,6 +813,8 @@
"SpeakV2Speed": ".types",
"SpeakV2SpeedValue": ".types",
"SpeakV2Tag": ".types",
"ThinkLiveSettingsV1": ".types",
"ThinkLiveSettingsV1Params": ".requests",
"ThinkSettingsV1": ".types",
"ThinkSettingsV1ContextLength": ".types",
"ThinkSettingsV1ContextLengthParams": ".requests",
Expand Down Expand Up @@ -892,7 +900,6 @@
}
)


def __getattr__(attr_name: str) -> typing.Any:
module_name = _dynamic_imports.get(attr_name)
if module_name is None:
Expand Down Expand Up @@ -990,6 +997,8 @@ def __dir__():
"DeepgramListenProviderV2Params",
"DeepgramModel",
"DeepgramParams",
"DeepgramThinkLiveProviderV1",
"DeepgramThinkLiveProviderV1Params",
"DefaultAioHttpClient",
"DefaultAsyncHttpxClient",
"DeleteAgentConfigurationV1Response",
Expand Down Expand Up @@ -1292,6 +1301,8 @@ def __dir__():
"SpeakV2Speed",
"SpeakV2SpeedValue",
"SpeakV2Tag",
"ThinkLiveSettingsV1",
"ThinkLiveSettingsV1Params",
"ThinkSettingsV1",
"ThinkSettingsV1ContextLength",
"ThinkSettingsV1ContextLengthParams",
Expand Down Expand Up @@ -1345,6 +1356,7 @@ def __dir__():
"voice_agent",
]


__all__.extend(
[
"SharedIntentsResults",
Expand Down
Loading
Loading