1212using System . Collections . Generic ;
1313using System . Diagnostics . CodeAnalysis ;
1414using System . IO ;
15+ using System . Runtime . ExceptionServices ;
1516using System . Threading ;
1617using System . Threading . Tasks ;
1718
@@ -23,61 +24,75 @@ namespace AWS.Speech.MEAI;
2324/// <see cref="VoiceAgentUpdate"/>s.
2425/// </summary>
2526/// <remarks>
26- /// The default backend is <see cref="VoiceAgentBackend.Pipeline"/>: Amazon Transcribe streaming for
27- /// STT, Amazon Bedrock via <c>AWS.Bedrock.MEAI</c> for reasoning, and Amazon Polly for TTS. Call
28- /// <see cref="Create"/> for the one-line default-chain factory, or use the constructor to compose any
29- /// MEAI clients you already have. Barge-in, the Nova Sonic backend swap, the <c>AsIRealtimeClient()</c>
30- /// adapter, and DI registration are wired in a later phase.
27+ /// The default backend is <see cref="VoiceAgentBackend.Pipeline"/>: Amazon Transcribe streaming for STT,
28+ /// Amazon Bedrock via <c>AWS.Bedrock.MEAI</c> for reasoning, and Amazon Polly for TTS. Set
29+ /// <see cref="VoiceAgentOptions.Backend"/> to <see cref="VoiceAgentBackend.NovaSonic"/> on
30+ /// <see cref="Create"/> to route the same <c>RunAsync</c> stream through Amazon Bedrock Nova Sonic
31+ /// instead. <see cref="AsIRealtimeClient"/> exposes either backend through MEAI's
32+ /// <see cref="IRealtimeClient"/> contract.
3133/// </remarks>
3234[ Experimental ( "MEAI001" ) ]
3335public sealed class VoiceAgent : IAsyncDisposable
3436{
35- private readonly ISpeechToTextClient _stt ;
36- private readonly IChatClient _chat ;
37- private readonly ITextToSpeechClient _tts ;
37+ private readonly ISpeechToTextClient ? _stt ;
38+ private readonly IChatClient ? _chat ;
39+ private readonly ITextToSpeechClient ? _tts ;
40+ private readonly IRealtimeClient ? _novaClient ;
3841 private readonly VoiceAgentOptions _options ;
3942 private readonly List < IDisposable > _ownedResources ;
4043 private int _disposed ;
4144
42- /// <summary>Initializes a provider-neutral <see cref="VoiceAgent"/> around any MEAI clients.</summary>
45+ /// <summary>Initializes a provider-neutral pipeline <see cref="VoiceAgent"/> around any MEAI clients.</summary>
4346 /// <exception cref="ArgumentNullException">A client is <see langword="null"/>.</exception>
4447 public VoiceAgent ( ISpeechToTextClient stt , IChatClient chat , ITextToSpeechClient tts , VoiceAgentOptions ? options = null )
45- : this ( stt , chat , tts , options ?? new VoiceAgentOptions ( ) , ownedResources : null )
48+ : this (
49+ stt ?? throw new ArgumentNullException ( nameof ( stt ) ) ,
50+ chat ?? throw new ArgumentNullException ( nameof ( chat ) ) ,
51+ tts ?? throw new ArgumentNullException ( nameof ( tts ) ) ,
52+ novaClient : null ,
53+ options ?? new VoiceAgentOptions ( ) ,
54+ ownedResources : null )
4655 {
4756 }
4857
49- private VoiceAgent ( ISpeechToTextClient stt , IChatClient chat , ITextToSpeechClient tts ,
58+ private VoiceAgent (
59+ ISpeechToTextClient ? stt , IChatClient ? chat , ITextToSpeechClient ? tts , IRealtimeClient ? novaClient ,
5060 VoiceAgentOptions options , List < IDisposable > ? ownedResources )
5161 {
52- _stt = stt ?? throw new ArgumentNullException ( nameof ( stt ) ) ;
53- _chat = chat ?? throw new ArgumentNullException ( nameof ( chat ) ) ;
54- _tts = tts ?? throw new ArgumentNullException ( nameof ( tts ) ) ;
62+ _stt = stt ;
63+ _chat = chat ;
64+ _tts = tts ;
65+ _novaClient = novaClient ;
5566 _options = options ;
5667 _ownedResources = ownedResources ?? new List < IDisposable > ( ) ;
5768 }
5869
5970 /// <summary>Creates a <see cref="VoiceAgent"/> with the default AWS credential and region chains.</summary>
6071 /// <remarks>
61- /// Constructs an Amazon Transcribe streaming client, an Amazon Bedrock runtime client (adapted via
62- /// <c>AmazonBedrockRuntimeExtensions.AsIChatClient</c>), and an Amazon Polly client, honoring
63- /// <see cref="VoiceAgentOptions.Credentials"/> and <see cref="VoiceAgentOptions.Region"/> when set.
64- /// A pre-built client on the options object wins over the default AWS client for that leg. The
65- /// returned agent owns any clients it constructed and disposes them on <see cref="DisposeAsync"/>.
72+ /// For <see cref="VoiceAgentBackend.Pipeline"/> this constructs an Amazon Transcribe streaming client,
73+ /// an Amazon Bedrock runtime client adapted to an <see cref="IChatClient"/>, and an Amazon Polly client.
74+ /// For <see cref="VoiceAgentBackend.NovaSonic"/> it constructs an Amazon Bedrock runtime client adapted
75+ /// to an <see cref="IRealtimeClient"/>. Both honor <see cref="VoiceAgentOptions.Credentials"/> and
76+ /// <see cref="VoiceAgentOptions.Region"/>, and a pre-built client on the options wins over the default
77+ /// AWS client for its leg. The returned agent owns any clients it constructed and disposes them on
78+ /// <see cref="DisposeAsync"/>.
6679 /// </remarks>
67- /// <exception cref="NotSupportedException">The requested backend is not yet available in this preview.</exception>
6880 public static VoiceAgent Create ( Action < VoiceAgentOptions > ? configure = null )
6981 {
7082 var options = new VoiceAgentOptions ( ) ;
7183 configure ? . Invoke ( options ) ;
7284
73- if ( options . Backend != VoiceAgentBackend . Pipeline )
85+ var owned = new List < IDisposable > ( ) ;
86+
87+ if ( options . Backend == VoiceAgentBackend . NovaSonic )
7488 {
75- throw new NotSupportedException (
76- $ "The { options . Backend } backend is not available yet in this preview. Use VoiceAgentBackend.Pipeline.") ;
89+ var bedrock = CreateBedrockClient ( options . Credentials , options . Region ) ;
90+ owned . Add ( bedrock ) ;
91+ var nova = bedrock . AsIRealtimeClient ( options . ModelId ) ;
92+ if ( nova is IDisposable disposableNova ) owned . Add ( disposableNova ) ;
93+ return new VoiceAgent ( stt : null , chat : null , tts : null , nova , options , owned ) ;
7794 }
7895
79- var owned = new List < IDisposable > ( ) ;
80-
8196 var stt = options . SpeechToTextClient ;
8297 if ( stt is null )
8398 {
@@ -102,24 +117,48 @@ public static VoiceAgent Create(Action<VoiceAgentOptions>? configure = null)
102117 tts = polly . AsITextToSpeechClient ( options . Voice , Engine . Neural , options . OutputSampleRateHertz ) ;
103118 }
104119
105- return new VoiceAgent ( stt , chat , tts , options , owned ) ;
120+ return new VoiceAgent ( stt , chat , tts , novaClient : null , options , owned ) ;
106121 }
107122
108123 /// <summary>Runs the voice loop over the caller's microphone PCM stream.</summary>
109124 /// <param name="microphonePcm">
110- /// Input audio at <see cref="VoiceAgentOptions.InputSampleRateHertz"/>, 16-bit signed
111- /// little-endian mono PCM. The agent never disposes this stream.
125+ /// Input audio at <see cref="VoiceAgentOptions.InputSampleRateHertz"/>, 16-bit signed little-endian
126+ /// mono PCM. The agent never disposes this stream.
112127 /// </param>
113128 /// <param name="cancellationToken">Stops the loop; the returned enumerable then completes.</param>
114129 /// <returns>One ordered stream of <see cref="VoiceAgentUpdate"/>s.</returns>
115130 /// <exception cref="ArgumentNullException"><paramref name="microphonePcm"/> is <see langword="null"/>.</exception>
116131 /// <exception cref="ObjectDisposedException">The agent has been disposed.</exception>
132+ /// <exception cref="NotSupportedException">
133+ /// The agent is configured for <see cref="VoiceAgentBackend.NovaSonic"/> but was built with the
134+ /// provider-neutral constructor, which is pipeline-only. Use <see cref="Create"/> for Nova Sonic.
135+ /// </exception>
117136 public IAsyncEnumerable < VoiceAgentUpdate > RunAsync ( Stream microphonePcm , CancellationToken cancellationToken = default )
118137 {
119138 if ( microphonePcm is null ) throw new ArgumentNullException ( nameof ( microphonePcm ) ) ;
120139 ThrowIfDisposed ( ) ;
121140
122- return VoiceAgentPipeline . RunAsync ( _stt , _chat , _tts , _options , microphonePcm , cancellationToken ) ;
141+ if ( _options . Backend == VoiceAgentBackend . NovaSonic )
142+ {
143+ if ( _novaClient is null )
144+ {
145+ throw new NotSupportedException (
146+ "The NovaSonic backend requires VoiceAgent.Create; the provider-neutral constructor is pipeline-only." ) ;
147+ }
148+ return NovaSonicRunner . RunAsync ( _novaClient , _options , microphonePcm , cancellationToken ) ;
149+ }
150+
151+ return VoiceAgentPipeline . RunAsync ( _stt ! , _chat ! , _tts ! , _options , microphonePcm , cancellationToken ) ;
152+ }
153+
154+ /// <summary>Exposes this agent through MEAI's <see cref="IRealtimeClient"/> contract.</summary>
155+ /// <param name="defaultModelId">An optional default model ID recorded on the adapter.</param>
156+ /// <returns>An <see cref="IRealtimeClient"/> whose sessions drive this agent's loop.</returns>
157+ /// <exception cref="ObjectDisposedException">The agent has been disposed.</exception>
158+ public IRealtimeClient AsIRealtimeClient ( string ? defaultModelId = null )
159+ {
160+ ThrowIfDisposed ( ) ;
161+ return new VoiceAgentRealtimeClient ( this , defaultModelId ) ;
123162 }
124163
125164 /// <summary>Returns the underlying MEAI client for the requested service type, or <see langword="null"/>.</summary>
@@ -131,6 +170,7 @@ public IAsyncEnumerable<VoiceAgentUpdate> RunAsync(Stream microphonePcm, Cancell
131170 if ( serviceType == typeof ( ISpeechToTextClient ) ) return _stt ;
132171 if ( serviceType == typeof ( IChatClient ) ) return _chat ;
133172 if ( serviceType == typeof ( ITextToSpeechClient ) ) return _tts ;
173+ if ( serviceType == typeof ( IRealtimeClient ) ) return _novaClient ;
134174 return serviceType . IsInstanceOfType ( this ) ? this : null ;
135175 }
136176
@@ -149,7 +189,7 @@ public ValueTask DisposeAsync()
149189
150190 if ( first is not null )
151191 {
152- System . Runtime . ExceptionServices . ExceptionDispatchInfo . Capture ( first ) . Throw ( ) ;
192+ ExceptionDispatchInfo . Capture ( first ) . Throw ( ) ;
153193 }
154194 return default ;
155195 }
0 commit comments