Skip to content
Draft
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
3 changes: 3 additions & 0 deletions cli/azd/extensions/azure.ai.agents/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ words:
# Voice (prompt-voice) agents
- BYOM
- Nanami
- pcma
- pcmu
- webrtc
# Azure region names
- australiaeast
- brazilsouth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -704,9 +704,10 @@ func readAgentServices(prior []Result) []string {

// filterHostedAgentServices removes prompt-voice services from the hosted-agent
// status probe. The remote.agent-status check targets hosted agent versions
// (`/agents/{name}/versions/{version}`) and relies on AGENT_<KEY>_VERSION; a
// prompt-voice deploy writes NAME+ENDPOINT only and should be covered by a
// voice-specific doctor check in a future PR.
// (`/agents/{name}/versions/{version}`). Unified prompt-voice deploys record a
// VERSION, but voice readiness follows its WebSocket endpoint rather than the
// hosted-agent version lifecycle and should be covered by a voice-specific
// doctor check in a future PR.
func filterHostedAgentServices(ctx context.Context, azdClient *azdext.AzdClient, services []string) []string {
if len(services) == 0 || azdClient == nil {
return services
Expand Down
33 changes: 16 additions & 17 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/nextstep/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ const (
agentVersionVarFormat = "AGENT_%s_VERSION"

// agentEndpointVarFormat is the base endpoint env-var written for every
// deployed agent. Voice agents (kind: prompt-voice) are created
// synchronously with no agent-version object, so this base endpoint is the
// only deployment marker they set — isDeployed falls back to it.
// deployed agent. Voice agents (kind: prompt-voice) use it as the deploy
// completion marker. Unified voice deploys also set VERSION, while legacy
// voice environments may have only this endpoint marker.
agentEndpointVarFormat = "AGENT_%s_ENDPOINT"

// projectEndpointVar is the env-var that carries the Foundry project
Expand Down Expand Up @@ -822,23 +822,22 @@ func isDeployed(
*errs = append(*errs, fmt.Errorf("read %s: %w", key, err))
return false
}
if value != "" {
return true
}

// Voice agents (kind: prompt-voice) deploy without an agent-version object,
// so they never set AGENT_<KEY>_VERSION. Fall back to the base endpoint
// marker, which every voice deploy writes, so a successfully created voice
// agent is not reported as undeployed. Gate this on the service's actual
// declared kind: a hosted agent whose deploy partially failed can also
// present an empty VERSION with a lingering ENDPOINT, and must stay reported
// as not-deployed. This mirrors the kind gate in
if !isVoice {
return value != ""
}

// Voice deploys use the base ENDPOINT env var as the completion marker.
// Unified voice deploys write VERSION before ENDPOINT to keep ENDPOINT as the
// final marker.
// Require ENDPOINT for voice even when VERSION is present, otherwise a partial
// env write could be reported as deployed before the callable endpoint was
// persisted. Gate this on the service's actual declared kind: a hosted agent
// whose deploy partially failed can also present an empty VERSION with a
// lingering ENDPOINT, and must stay reported as not-deployed. This mirrors the
// kind gate in
// AgentServiceTargetProvider.Endpoints (project package); the two live in
// separate packages because project imports nextstep, so a literally shared
// helper would create an import cycle.
if !isVoice {
return false
}
endpointKey := fmt.Sprintf(agentEndpointVarFormat, serviceKey(serviceName))
endpointValue, err := src.EnvValue(ctx, envName, endpointKey)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -739,10 +739,10 @@ func TestServiceKey(t *testing.T) {
}
}

// TestIsDeployed_VoiceEndpointFallback verifies that a voice agent — which sets
// only AGENT_<KEY>_NAME and AGENT_<KEY>_ENDPOINT, never AGENT_<KEY>_VERSION — is
// still reported as deployed via the base endpoint marker, while an agent with
// neither version nor endpoint is reported undeployed.
// TestIsDeployed_VoiceEndpointFallback verifies that voice readiness is based
// on the base endpoint marker. Legacy voice agents never set VERSION, while
// unified voice agents set VERSION before ENDPOINT; in both cases ENDPOINT is
// the deploy completion marker.
func TestIsDeployed_VoiceEndpointFallback(t *testing.T) {
t.Parallel()

Expand All @@ -757,6 +757,21 @@ func TestIsDeployed_VoiceEndpointFallback(t *testing.T) {
values: map[string]string{"env1/AGENT_VOICE_SVC_VERSION": "1"},
want: true,
},
{
name: "version set but endpoint missing: undeployed (voice agent partial write)",
values: map[string]string{"env1/AGENT_VOICE_SVC_VERSION": "1"},
isVoice: true,
want: false,
},
{
name: "version and endpoint set: deployed (unified voice agent)",
values: map[string]string{
"env1/AGENT_VOICE_SVC_VERSION": "1",
"env1/AGENT_VOICE_SVC_ENDPOINT": "wss://x/agents/a/endpoint/protocols/voice?api-version=v1",
},
isVoice: true,
want: true,
},
{
name: "no version but base endpoint set: deployed (voice agent)",
values: map[string]string{"env1/AGENT_VOICE_SVC_ENDPOINT": "https://x/voice_agents/a"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,9 @@ const (
AgentKindHosted AgentKind = "hosted"
AgentKindWorkflow AgentKind = "workflow"
// AgentKindVoice is the data-plane (service) kind for a declarative voice
// (speech-to-speech) agent. Note: this is the wire value posted to the
// /voice_agents collection. The azd manifest authoring kind is
// "prompt-voice" (agent_yaml.AgentKindPromptVoice), which the map layer
// translates to this value.
// (speech-to-speech) agent. The azd manifest authoring kind is "prompt-voice"
// (agent_yaml.AgentKindPromptVoice), which the map layer translates to this
// value before posting through the unified /agents API.
AgentKindVoice AgentKind = "voice"
)

Expand Down Expand Up @@ -360,7 +359,7 @@ const (
// VoiceAudioFormat describes a PCM audio stream format (e.g. audio/pcm @ 24 kHz).
type VoiceAudioFormat struct {
Type string `json:"type"`
Rate int `json:"rate"`
Rate *int `json:"rate,omitempty"`
}

// VoiceTurnDetection configures server-side voice-activity detection so the
Expand All @@ -370,32 +369,69 @@ type VoiceTurnDetection struct {
Threshold *float64 `json:"threshold,omitempty"`
PrefixPaddingMs *int `json:"prefix_padding_ms,omitempty"`
SilenceDurationMs *int `json:"silence_duration_ms,omitempty"`
CreateResponse *bool `json:"create_response,omitempty"`
Eagerness *string `json:"eagerness,omitempty"`
SpeechDurationMs *int `json:"speech_duration_ms,omitempty"`
RemoveFillerWords *bool `json:"remove_filler_words,omitempty"`
InterruptResponse *bool `json:"interrupt_response,omitempty"`
Languages []string `json:"languages,omitempty"`
AutoTruncate *bool `json:"auto_truncate,omitempty"`
}

// VoiceTranscription enables user-speech transcription events on the input stream.
type VoiceTranscription struct {
Model string `json:"model,omitempty"`
Model string `json:"model,omitempty"`
Language *string `json:"language,omitempty"`
Prompt *string `json:"prompt,omitempty"`
}

// VoiceNoiseReduction configures input audio noise reduction.
type VoiceNoiseReduction struct {
Type string `json:"type"`
}

// VoiceInputConfig is the input (caller -> agent) audio configuration.
type VoiceInputConfig struct {
Format *VoiceAudioFormat `json:"format,omitempty"`
TurnDetection *VoiceTurnDetection `json:"turn_detection,omitempty"`
Transcription *VoiceTranscription `json:"transcription,omitempty"`
Format *VoiceAudioFormat `json:"format,omitempty"`
NoiseReduction *VoiceNoiseReduction `json:"noise_reduction,omitempty"`
EchoCancellation map[string]any `json:"echo_cancellation,omitempty"`
TurnDetection *VoiceTurnDetection `json:"turn_detection,omitempty"`
Transcription *VoiceTranscription `json:"transcription,omitempty"`
}

// VoiceConfig selects the output voice. Type is "openai" for realtime voices
// (single lowercase word, e.g. "alloy") or "azure_standard" for Azure Neural
// voices (e.g. "en-US-Ava:DragonHDLatestNeural").
type VoiceConfig struct {
Type string `json:"type"`
Name string `json:"name"`
Type string `json:"type"`
Name string `json:"name"`
Style *string `json:"style,omitempty"`
Pitch *string `json:"pitch,omitempty"`
Rate *string `json:"rate,omitempty"`
Locale *string `json:"locale,omitempty"`
Volume *string `json:"volume,omitempty"`
}

// VoiceOutputConfig is the output (agent -> caller) audio configuration.
type VoiceOutputConfig struct {
Format *VoiceAudioFormat `json:"format,omitempty"`
Voice *VoiceConfig `json:"voice,omitempty"`
Speed *float64 `json:"speed,omitempty"`
}

// VoiceOutputConfigFlat is the newer Voice Live output shape used by the
// unified /agents voice API in TiP. Older regions still accept/return the
// object-shaped VoiceOutputConfig above.
type VoiceOutputConfigFlat struct {
Format *VoiceAudioFormat `json:"format,omitempty"`
Voice string `json:"voice,omitempty"`
VoiceType string `json:"voice_type,omitempty"`
VoiceLocale string `json:"voice_locale,omitempty"`
Style *string `json:"style,omitempty"`
Pitch *string `json:"pitch,omitempty"`
Rate *string `json:"rate,omitempty"`
Volume *string `json:"volume,omitempty"`
Speed *float64 `json:"speed,omitempty"`
}

// VoiceAudioConfig bundles the input and output audio configuration.
Expand All @@ -404,17 +440,54 @@ type VoiceAudioConfig struct {
Output *VoiceOutputConfig `json:"output,omitempty"`
}

// VoiceAgentDefinition is the data-plane definition body POSTed to the
// /voice_agents collection for a declarative (managed) voice agent. Its Kind
// is always AgentKindVoice ("voice").
// VoiceAudioConfigFlat bundles voice audio config with the flat output shape.
type VoiceAudioConfigFlat struct {
Input *VoiceInputConfig `json:"input,omitempty"`
Output *VoiceOutputConfigFlat `json:"output,omitempty"`
}

// VoiceAgentDefinition is retained for compatibility with object-shaped voice
// definitions returned by older services. New prompt voice deploys use
// VoiceAgentDefinitionFlat.
type VoiceAgentDefinition struct {
AgentDefinition
ModelType VoiceModelType `json:"model_type"`
Model string `json:"model"`
Instructions string `json:"instructions,omitempty"`
Audio *VoiceAudioConfig `json:"audio,omitempty"`
OutputModalities []string `json:"output_modalities,omitempty"`
Store *bool `json:"store,omitempty"`
ModelType VoiceModelType `json:"model_type"`
Model string `json:"model"`
Instructions string `json:"instructions,omitempty"`
StructuredInputs map[string]any `json:"structured_inputs,omitempty"`
Audio *VoiceAudioConfig `json:"audio,omitempty"`
OutputModalities []string `json:"output_modalities,omitempty"`
Store *bool `json:"store,omitempty"`
Tools []map[string]any `json:"tools,omitempty"`
Avatar map[string]any `json:"avatar,omitempty"`
Greeting map[string]any `json:"greeting,omitempty"`
Handoff map[string]any `json:"handoff,omitempty"`
ToolChoice any `json:"tool_choice,omitempty"`
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
MaxOutputTokens any `json:"max_output_tokens,omitempty"`
Include []string `json:"include,omitempty"`
}

// VoiceAgentDefinitionFlat is the voice definition shape aligned with the
// unified /agents TiP API, where audio.output.voice is a string and the voice
// provider details are sibling fields.
type VoiceAgentDefinitionFlat struct {
AgentDefinition
ModelType VoiceModelType `json:"model_type"`
Model string `json:"model"`
Instructions string `json:"instructions,omitempty"`
StructuredInputs map[string]any `json:"structured_inputs,omitempty"`
Audio *VoiceAudioConfigFlat `json:"audio,omitempty"`
OutputModalities []string `json:"output_modalities,omitempty"`
Store *bool `json:"store,omitempty"`
Tools []map[string]any `json:"tools,omitempty"`
Avatar map[string]any `json:"avatar,omitempty"`
Greeting map[string]any `json:"greeting,omitempty"`
Handoff map[string]any `json:"handoff,omitempty"`
ToolChoice any `json:"tool_choice,omitempty"`
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
MaxOutputTokens any `json:"max_output_tokens,omitempty"`
Include []string `json:"include,omitempty"`
}

// CreateAgentVersionRequest represents a request to create an agent version
Expand Down
Loading
Loading