Skip to content

[azure-ai-projects] Add Voice Agents realtime client, samples, and tests aligned with .beta structure - #48965

Draft
xitzhang (xitzhang) wants to merge 4 commits into
feature/azure-ai-projects/vnextfrom
xitzhang/voice-agents-beta
Draft

[azure-ai-projects] Add Voice Agents realtime client, samples, and tests aligned with .beta structure#48965
xitzhang (xitzhang) wants to merge 4 commits into
feature/azure-ai-projects/vnextfrom
xitzhang/voice-agents-beta

Conversation

@xitzhang

Copy link
Copy Markdown
Member

Summary

Adds Voice Agents to �zure-ai-projects, aligned with the .beta naming/structure already established on this branch ( eature/azure-ai-projects/vnext picked up the generated Voice Agents surface via #48950). This PR adds the pieces needed to make it a complete, usable feature: the hand-written realtime WebSocket client, samples, tests, and docs.

Related to #47803 (tracking �next -> main). cc Darren Cohen (@dargilco)

What's included

  • client.beta.realtime / �sync_client.beta.realtime: hand-written WebSocket client (Realtime/AsyncRealtime, RealtimeConnection(Manager)) exposed as a property on BetaOperations, exported from �zure.ai.projects.operations / �zure.ai.projects.aio.operations (matching how other .beta-only classes are exported).
  • 11 samples under samples/agents/voice/: CRUD lifecycle, guided generation (.beta.agents.generate), versioning, tools, live text/audio conversations, function-tool calling, and reading back persisted conversations/audio via .beta.agent_endpoint_conversations.
  • 14 test files under ests/agents/ and ests/foundry_features_header/: CRUD, telephony, telephony campaigns, conversations, realtime client (mocked + live), and telephony protocol tests.
  • Test infra: conftest.py sanitizers, est_base.py env var, and a NON_OPERATION_BETA_ATTRIBUTES exclusion in the generic .beta header-injection test (
    ealtime is a hand-written WebSocket entry point, not a generated REST operation, so it needs its own dedicated header test instead -- see est_realtime_client.py).
  • PostEmitter.ps1: regression guard so a future sp-client update can't silently overwrite _realtime.py's SDK client-identification fix (Add Voice Live SDK client identification #48848) without failing the emit step.
  • pyproject.toml: optional
    ealtime extra (websockets / �iohttp).
  • CHANGELOG.md, README.md, .env.template, dev_requirements.txt updated.

Renames applied while porting

This branch's TypeSpec commit turned out to be newer than the source content this was ported from, and had renamed several methods beyond just .beta nesting (verified against this branch's own generated code and docs/public-methods.md):

  • �gent_endpoint_conversations: dropped the �gent_conversation infix and renamed content methods to download (e.g. get_agent_conversation_item -> get_item, get_agent_conversation_item_audio_content -> download_item_audio).
  • �gent_telephony: dropped the redundant elephony_ infix (e.g. create_telephony_call_job -> create_call_job, �egin_validate_telephony_campaign -> �egin_validate_campaign).
  • �gents.generate_agent -> �gents.generate, and telephony binding/call methods moved from top-level client.agents to client.beta.agents.

Validation

  • �lack / pylint / mypy / pyright: all clean.
  • Full test suite: 219 passed / 74 skipped / 0 failed outside voice; voice + header suite 732 passed / 49 skipped / 8 failed -- all 8 failures are brand-new tests with no recording yet (not a regression; this package's convention is to keep new tests enabled so PR validation surfaces missing recordings).
  • Live end-to-end tested all 11 samples against a real Foundry project with a real gpt-realtime deployment: real HTTP status codes for REST CRUD, real multi-turn realtime conversations with byte-accurate audio sizing (format/sample-rate/channels/duration), real function-tool invocation (verified get_weather called and incorporated into the reply), and real conversation/audio persistence + readback.

…ta structure

feature/azure-ai-projects/vnext already has the generated Voice Agents surface
(.beta.agent_endpoint_conversations, .beta.agent_telephony, .beta.agents
telephony binding/call/generate methods, .beta.voice_agent_web_socket) via
PR #48950. This change adds the missing hand-written realtime WebSocket
client and the surrounding samples/tests/docs to make Voice Agents a complete,
usable feature, all under the .beta naming convention:

- azure/ai/projects/_realtime.py and aio/_realtime.py: hand-written realtime
  WebSocket client (Realtime/AsyncRealtime, RealtimeConnection(Manager)),
  exposed as client.beta.realtime / async_client.beta.realtime via a new
  property on BetaOperations. Exported from azure.ai.projects.operations /
  azure.ai.projects.aio.operations (not the top-level package), matching how
  other .beta-only classes are exported.
- 11 samples under samples/agents/voice/: CRUD lifecycle, guided generation,
  versioning, tools, live text/audio conversations, function-tool calling,
  and reading back persisted conversations/audio.
- 14 test files under tests/agents/ and tests/foundry_features_header/:
  CRUD, telephony, telephony campaigns, conversations, realtime client
  (mocked + live), and telephony protocol tests.
- Supporting test infra: conftest.py Foundry-Features/Accept sanitizers,
  test_base.py foundry_voice_model_name, and a NON_OPERATION_BETA_ATTRIBUTES
  exclusion in the generic .beta header-injection test (realtime is a
  hand-written WebSocket entry point, not a generated REST operation, so it
  can't be exercised by that generic mechanism -- it has its own dedicated
  header test in test_realtime_client.py).
- PostEmitter.ps1: regression guard so a future tsp-client update can't
  silently overwrite _realtime.py's SDK client-identification fix (PR #48848)
  without failing the emit step.
- pyproject.toml: optional 
ealtime extra (websockets / aiohttp).
- CHANGELOG.md, README.md, .env.template, dev_requirements.txt updated.

Renames applied while porting (vnext's TypeSpec commit is newer than the
source branch this was ported from, and renamed several methods beyond just
.beta nesting -- verified against vnext's own generated code and
docs/public-methods.md):
- agent_endpoint_conversations: dropped the "agent_conversation" infix and
  renamed *_content methods to download_* (e.g. get_agent_conversation_item
  -> get_item, get_agent_conversation_item_audio_content ->
  download_item_audio).
- agent_telephony: dropped the redundant "telephony_" infix (e.g.
  create_telephony_call_job -> create_call_job, begin_validate_telephony_campaign
  -> begin_validate_campaign).
- agents.generate_agent -> agents.generate, and telephony binding/call
  methods moved from top-level client.agents to client.beta.agents.

Validated: black/pylint/mypy/pyright all clean. Full test suite: 219
passed/74 skipped/0 failed outside voice; voice+header suite 732 passed/49
skipped/8 failed (all 8 are brand-new tests with no recording yet, not a
regression). Live-tested all 11 samples end-to-end against a real Foundry
project with a real gpt-realtime deployment: real HTTP status codes, real
multi-turn realtime conversations with byte-accurate audio sizing, real
function-tool invocation, and real conversation/audio persistence+readback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
10 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor
[Pilot] PR Pipeline Failure Analysis

What failed

Pipeline build 6812287 failed on all platforms (macos311, Ubuntu2404_313, ubuntu2404_310, ubuntu2404_310_coverage, Ubuntu2404_314, windows2022_312), each with two failure sources for azure-ai-projects:

  1. CSpell lint failure (validation step, fixable) - the validation step exits with code 1 due to an unknown word flagged in a new sample file:
    sdk/ai/azure-ai-projects/samples/agents/voice/sample_voice_agent_live_audio_conversation_async.py:104:44 - Unknown word (redef)
  2. Missing test recordings (96 identical failures across all platforms/artifacts, not fixable by code change) - every one of the 8 new voice-agent tests fails identically on every platform/artifact (sdist and whl) because no recording file exists yet for these brand-new tests.
Relevant pipeline output
##[error]sdk/ai/azure-ai-projects/samples/agents/voice/sample_voice_agent_live_audio_conversation_async.py:104:44 - Unknown word (redef)
##[error]Spelling errors detected. To correct false positives or learn about spell checking see: https://aka.ms/azsdk/engsys/spellcheck
##[error]PowerShell exited with code 1.

Failing tests (identical on every platform / sdist+whl artifact):
sdk.ai.azure-ai-projects.tests.agents.test_voice_agent_conversations.TestVoiceAgentConversations.test_read_conversation
sdk.ai.azure-ai-projects.tests.agents.test_voice_agent_conversations_async.TestVoiceAgentConversationsAsync.test_read_conversation_async
sdk.ai.azure-ai-projects.tests.agents.test_voice_agent_crud.TestVoiceAgentCrud.test_voice_agent_crud
sdk.ai.azure-ai-projects.tests.agents.test_voice_agent_crud.TestVoiceAgentCrud.test_voice_agent_disable_enable
sdk.ai.azure-ai-projects.tests.agents.test_voice_agent_crud.TestVoiceAgentCrud.test_generate_agent
sdk.ai.azure-ai-projects.tests.agents.test_voice_agent_crud_async.TestVoiceAgentCrudAsync.test_voice_agent_crud_async
sdk.ai.azure-ai-projects.tests.agents.test_voice_agent_crud_async.TestVoiceAgentCrudAsync.test_voice_agent_disable_enable_async
sdk.ai.azure-ai-projects.tests.agents.test_voice_agent_crud_async.TestVoiceAgentCrudAsync.test_generate_agent_async

Sample error:
azure.core.exceptions.HttpResponseError: Recording file path .../sdk/ai/azure-ai-projects/tests/recordings/agents/test_voice_agent_conversations.pyTestVoiceAgentConversationstest_read_conversation.json does not exist. Status NotFound

Recommended next steps

  • Fix the cspell false positive by adding redef (or the intended correctly-spelled word) to the repo's cspell dictionary/ignore list, or rewording the flagged text at sdk/ai/azure-ai-projects/samples/agents/voice/sample_voice_agent_live_audio_conversation_async.py:104:44.
  • The 8 new voice-agent tests have no recordings yet. This matches the PR author's own note that these are brand-new tests kept enabled to surface missing recordings - not a regression. Recordings need to be generated by running the tests in live/record mode against a real Foundry project with valid Azure credentials, then committing the resulting recording JSON files (or updating the assets.json used by the recording proxy's external assets store).
  • See https://aka.ms/ci-fix

Automated fix: Fix found, view and apply fix

Generated by Pipeline Analysis Next Steps · auto · 162.9 AIC · ⌖ 2 AIC · ⊞ 9.2K ·

Comment thread sdk/ai/azure-ai-projects/PostEmitter.ps1 Outdated
Comment thread sdk/ai/azure-ai-projects/pyproject.toml Outdated
model=model,
instructions="You are a friendly voice assistant. Keep replies short and natural.",
audio=VoiceAgentAudioConfig(
output=VoiceAgentAudioOutputConfig(voice="en-US-AvaNeural", voice_type=VoiceType.AZURE_STANDARD),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Regarding voice="en-US-AvaNeural" I expected to see a selection from an enum instead of writing a string. Do we defined all voices as a union in TypeSpec? If not, why?

@xitzhang xitzhang (xitzhang) Sep 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I checked the generated model: voice is intentionally Optional[str], while voice_type is already an extensible VoiceType enum. I think service added this is also to avoid unnecessary API version changes when voices are added or removed. Since this comes from TypeSpec, we can revisit it at the spec level if needed.

DefaultAzureCredential() as credential,
AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project_client,
):
agent = project_client.beta.agents.generate(GenerateVoiceAgentRequest(kind=AgentKind.VOICE, name=agent_name))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

".agents.generate" is a too generic name. We'll see where it lands in our sub-client discussions. Depending on what sub-client it's on, we may or may not need a more descriptive name. If it's ".beta.voice-agent.generate" then the name is probably okay.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also can we eliminate kind as parameter?

@xitzhang xitzhang (xitzhang) Sep 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Makes sense -- this one's out of my hands in this PR though: .beta.agents.generate is already the shape that landed in the generated code this branch picked up (from #48950). I’ll summarize the fixes related to the TypeSpec.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Also can we eliminate kind as parameter?

Agreed — kind can only ever be voice here, so it shouldn't need to be passed. Same root cause as the discriminator comment above: TypeSpec currently defines kind as a plain required field instead of a discriminator (or a field with a default), so the generator makes it a required constructor arg. Fixing it needs a TypeSpec change; I'll flag it there rather than hand-patch the generated model here.

DefaultAzureCredential() as credential,
AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project_client,
):
agent = project_client.beta.agents.generate(GenerateVoiceAgentRequest(kind=AgentKind.VOICE, name=agent_name))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also can we eliminate kind as parameter?

# session's conversation can be fetched back by id afterward. Reuse the generated
# definition as-is (instead of reconstructing a new one from a few fields) so audio,
# greeting, tools, and any other service-selected settings are preserved.
definition.store = True # type: ignore[attr-defined]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why can't I just pass "store=True" to GenerateVoiceAgentRequest() above and get the agent I want, instead of creating one version without store, then another version with store?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

My bad, generate() has no store field on purpose — it's meant to stay minimal. I'd used generate() + a second create_version() while testing, and it's still in the sample.
Fixed: it now calls create_version() directly with VoiceAgentDefinition(..., store=True) in one step, matching sample_voice_agent_basic.py.

# definition as-is (instead of reconstructing a new one from a few fields) so audio,
# greeting, tools, and any other service-selected settings are preserved.
definition.store = True # type: ignore[attr-defined]
project_client.agents.create_version(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should create_version internally calls generate, and make generate private?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good question! I'd keep them separate — generate is for AI-assisted scaffolding, create_version needs to stay generic across all agent kinds. Also I will use create agent api here, not generate and change version. Will update samples

Comment thread sdk/ai/azure-ai-projects/azure/ai/projects/_realtime.py Outdated
foundry_features: str = _VOICE_AGENT_FEATURE_HEADER,
agent_session_id: Optional[str] = None,
agent_version_override: Optional[str] = None,
structured_inputs: Optional[str] = None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the name seems the type should be an array. But in the docstr, it should be JSON.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, real bug — it was typed Optional[str], silently requiring callers to pre-serialize their own JSON string with no documentation of the expected shape. Retyped to Optional[Mapping[str, Any]]. Fixed in both sync and async

api_version: Optional[str] = None,
credential_scopes: Optional[List[str]] = None,
extra_query: Optional[Mapping[str, str]] = None,
extra_headers: Optional[Mapping[str, str]] = None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Darren Cohen (@dargilco) should extra headers and extra query part of kwargs for our standard?

Review feedback fixes:
- CSpell: add "redef" to sdk/ai/cspell.yaml (fixes CI failure on the
  type: ignore[no-redef] comment in
  sample_voice_agent_live_audio_conversation_async.py).
- PostEmitter.ps1: remove the _realtime.py regression guard -- verified
  empirically (ran a real tsp-client update against the pinned commit; the
  hand-written files came back byte-identical) that tsp-client update does
  not touch files it doesn't generate, so the guard was unnecessary.
- pyproject.toml: rename the "realtime" optional-dependency extra to
  "voice" (matches how it'll be documented for voice agents); update the
  2 samples that referenced [realtime] to [voice].
- Samples: pin azure-ai-projects>=2.7.0 (drop the b1/--pre prerelease
  pins -- this package no longer ships beta releases) across all 11 voice
  samples; use the [voice] extra instead of manually listing aiohttp in
  the 2 samples that need it.
- _realtime.py / aio/_realtime.py:
  - Remove the foundry_features parameter from
    RealtimeConnectionManager/Realtime.connect/AsyncRealtime.connect --
    the realtime route is voice-agent-specific, so the header value is
    always the same and callers should not need (or be able) to override
    it. The value is now a hardcoded module constant.
  - Fix structured_inputs' type: it was Optional[str], silently requiring
    callers to pre-serialize their own JSON string with no documentation
    of the expected shape. Retyped to Optional[Mapping[str, Any]],
    matching the analogous generated model field
    (CreateTelephonyCallJobRequest.structured_inputs: dict[str, any]),
    and now serialized internally via SdkJSONEncoder.
  - Updated test_realtime_client.py/_async.py's _make_manager() helpers
    to match (foundry_features removed; the header-value assertions still
    pass since it's now a module constant rather than a per-call override).

Test recordings:
- Generated live recordings for the 8 new voice-agent tests that had none
  yet (test_voice_agent_crud(_async).py x6, test_read_conversation(_async)
  x2), against a real Foundry project with a real gpt-realtime deployment.
  Verified sanitization (endpoint/auth stripped) and re-verified end-to-end
  in playback mode after a fresh assets restore from the new tag.
- Pushed the new recordings to Azure/azure-sdk-assets (tag
  python/ai/azure-ai-projects_59c7584f68) via the GitHub Git Data REST API,
  since a direct git push to that repo hangs in this environment; updated
  assets.json accordingly.

Validated: black/pylint/mypy clean; full existing test suite unaffected
(740 passed, 49 skipped, 0 failed, up from 732 passed/8 failed).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@glharper Glenn Harper (glharper) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comments 2-6 from the local review, checked against the current PR revision.

Comment on lines +749 to +750
if self._structured_inputs is not None:
headers["x-ms-voice-structured-inputs"] = json.dumps(self._structured_inputs, cls=SdkJSONEncoder)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

2. [P2] Send structured inputs through the defined query parameter

connect(structured_inputs=...) serializes the mapping into an x-ms-voice-structured-inputs header, but the API contract defines a URL-encoded structured_input query parameter. Both the pinned TypeSpec route and the generated request builder specify that query parameter, not this header. Consequently, per-session instruction/greeting substitutions are not delivered through the defined service interface. The same issue exists in the async implementation.

Evidence: The original offline handshake captures showed no structured_input query entry, while the generated request builder given the same JSON produced that entry. The newer mapping-type change adds JSON serialization, but still writes only this header; the wire-contract mismatch remains.

Suggested fix: Serialize the mapping and add it to the query parameters under structured_input before URL encoding in both implementations. Keep the public parameter name if desired, but align its wire representation and handshake assertions with the generated contract.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, confirmed against the generated request builder — fixed. structured_inputs is now serialized into the structured_input query parameter in both _realtime.py and aio/_realtime.py, matching the pinned TypeSpec route.

Comment on lines +593 to +597
while True:
try:
yield self.recv()
except ConnectionResetError:
return

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

3. [P2] Propagate abnormal WebSocket termination from event iterators

Both iterators catch every ConnectionResetError and return normally. The synchronous recv() converts all websockets.ConnectionClosed exceptions, including abnormal closure, to that exception. The asynchronous recv() uses it for both closure frames and WSMsgType.ERROR. Thus server failures and interrupted connections become indistinguishable from successful end-of-stream in the advertised for / async for interface. A caller can silently accept a truncated response, and its exception-based recovery path never runs. The same catch-and-return occurs in the async iterator.

Evidence: A synchronous ConnectionClosedError with close code 1011 caused list(connection) to return []. An asynchronous close-1011 frame and an asynchronous error frame carrying an OSError likewise ended iteration without raising.

Suggested fix: End iteration quietly only for normal closure. Preserve and propagate abnormal close codes and receive errors, including the underlying cause, in both implementations.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in both implementations. recv() still raises ConnectionResetError for any closure, but _iter()/__aiter__ now only end iteration quietly for a graceful one — an abnormal close code (e.g. 1011) or a transport error now propagates instead of being silently swallowed. Added regression tests for both the graceful and abnormal paths (sync and async).

Comment on lines +343 to +345
if response_active:
await conn.response.cancel()
ap.skip_pending_audio()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

4. [P2] Clear buffered speaker audio on barge-in after generation completes

On input_audio_buffer.speech_started, ap.skip_pending_audio() runs only while response_active is true. That flag becomes false on response.done, which marks the end of server generation, not the end of local playback. When the server generates audio faster than the speaker consumes it, speaking over the remaining buffered reply does not interrupt playback; the old reply continues over the user.

Evidence: Exercising the actual sample audio callback with a fake device, two seconds of queued PCM, and only 50 ms consumed, then delivering response.done followed by speech_started, left the next callback returning old non-silent audio.

Suggested fix: Clear pending local playback on every speech-start event. Guard only the server-side response.cancel() call with response_active; local playback and server generation need independent state.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed — ap.skip_pending_audio() now runs unconditionally on speech_started, independent of response_active. Only the server-side response.cancel() call (and the "(listening...)" message) stay guarded by response_active, since cancelling with none active is a service error.

Comment on lines +201 to +204
except TimeoutError:
print("Timed out waiting for the agent's reply.")
conn.response.cancel()
return

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

5. [P2] Drain a cancelled response before accepting the next text turn

After a reply times out, the samples send response.cancel() and return to prompting without consuming the cancelled response's terminal event. The next turn's event pump returns on any RealtimeServerEventResponseDone, without matching the response ID. A late completion for the cancelled turn therefore ends the next turn's pump before the new reply is displayed, leaving the stream out of step with the user. The same issue exists in the async text sample.

Evidence: Deterministic sync and async runs supplied a first-turn timeout, the first response's cancelled response.done, and the second response's transcript/completion. Both issued two response requests but failed to display the second reply; the async run left its transcript and completion unread.

Suggested fix: Consume cancellation through the matching terminal event before reusing the stream, and correlate completion events with the active response ID. If cancellation cannot be confirmed within a bounded wait, end the session rather than accept another turn on an unsynchronized stream. Apply the same handling to the async greeting-timeout path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in both the sync and async samples (including the async greeting-timeout path). Each turn now tracks its response id from response.created, and after a timeout+cancel, a new bounded drain step consumes that response's terminal event (correlated by id) before the next turn starts — so a late completion can no longer be mistaken for the next turn's reply. If it can't be confirmed in time, the sample now ends the session cleanly instead of continuing on a stream it can't trust.

Comment on lines +224 to +226
except HttpResponseError as e:
if e.status_code == 404:
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

6. [P2] Require a successful per-item audio retrieval in the conversation tests

Both tests tolerate every per-item audio request returning 404 without requiring a single successful audio retrieval. Their setup requests a stored audio response and requires a completed conversation with a merged recording, but a broken per-item audio route can still pass the test without ever reaching download_item_audio(). Recording that behavior would preserve the false-positive result in playback. The same issue exists in the async test.

Evidence: Running both unchanged test bodies with mocked completed conversations containing user and assistant-audio items passed despite two item-audio 404s and zero item-audio download calls in each run. This reproduction bypassed live setup and recording decorators; it did not contact the service.

Suggested fix: Continue tolerating 404 for genuinely non-audio items, but require at least one successful audio-item metadata retrieval and a nonempty download for Foundry-managed storage.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in both the sync and async test — now tracks whether any item successfully retrieved audio and asserts that after the loop, so a fully-broken get_item_audio/download_item_audio route can no longer pass silently.

Xiting Zhang and others added 2 commits September 10, 2026 19:40
…nt samples to use create_version directly

- _realtime.py / aio/_realtime.py: send structured_inputs as the documented
  structured_input query parameter instead of a custom header; distinguish
  graceful WebSocket closure from abnormal closure/transport errors so
  `for event in conn:` no longer silently swallows real failures.
- sample_voice_agent_live_audio_conversation_async.py: always clear buffered
  local playback audio on barge-in, independent of whether a server response
  is still active.
- sample_voice_agent_live_text_conversation.py / _async.py: drain a cancelled
  response's terminal event (correlated by response id) before starting the
  next turn, so a late completion can't be mistaken for the next reply.
- test_voice_agent_conversations.py / _async.py: require at least one
  successful per-item audio retrieval instead of tolerating an all-404 run.
- test_realtime_client.py / _async.py: add regression tests for the
  structured_input query parameter and the graceful-vs-abnormal closure
  distinction.
- sample_voice_agent_live_audio_conversation_async.py, sample_voice_agent_live_text_conversation.py,
  sample_voice_agent_live_text_conversation_async.py: replace the generate()-then-patch-store
  two-step pattern with a single direct VoiceAgentDefinition(..., store=True) + create_version()
  call, matching sample_voice_agent_basic.py. Live-tested all three end-to-end.
- GeneratePublicMethods.ps1 / docs/public-methods.md / api.md / api.metadata.yml:
  fix the generator to recognize the hand-written beta.realtime sub-client
  (a plain property, not a generated *Operations group) and regenerate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants