Skip to content

Commit b83accd

Browse files
committed
feat: add streaming support
1 parent 6744685 commit b83accd

12 files changed

Lines changed: 534 additions & 56 deletions

File tree

AWS.AgentCore.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<Folder Name="/sampleapps/">
33
<Project Path="sampleapps/MicrosoftAgentFrameworkSample/MicrosoftAgentFrameworkSample.csproj" />
44
<Project Path="sampleapps/ChatBotUI/ChatBotUI.csproj" />
5+
<Project Path="sampleapps/StreamingAgent/StreamingAgent.csproj" />
56
</Folder>
67
<Folder Name="/src/">
78
<Project Path="src/AWS.AgentCore.SourceGenerator/AWS.AgentCore.SourceGenerator.csproj" />

sampleapps/ChatBotUI/Components/Pages/Home.razor

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@
2323
{
2424
<span class="chat-header-session">@CurrentSession.Title</span>
2525
}
26+
<div class="streaming-toggle" title="@(_useStreaming ? "Streaming mode (SSE)" : "Standard mode (JSON)")">
27+
<label class="toggle-label">
28+
<input type="checkbox" @bind="_useStreaming" />
29+
<span class="toggle-slider"></span>
30+
</label>
31+
<span class="toggle-text">@(_useStreaming ? "Streaming" : "Standard")</span>
32+
</div>
2633
</div>
2734
</header>
2835

@@ -183,6 +190,7 @@
183190
@code {
184191
private string _userInput = string.Empty;
185192
private bool _isLoading;
193+
private bool _useStreaming;
186194
private ElementReference _messagesContainer;
187195
private ElementReference _textInput;
188196
private CancellationTokenSource? _cts;
@@ -260,8 +268,20 @@
260268
{
261269
_cts = new CancellationTokenSource();
262270

263-
var response = await AgentService.InvokeAgentAsync(input, session.Id, _cts.Token);
264-
assistantMessage.Content = response;
271+
if (_useStreaming)
272+
{
273+
await foreach (var chunk in AgentService.InvokeAgentStreamingAsync(input, session.Id, _cts.Token))
274+
{
275+
assistantMessage.Content += chunk;
276+
StateHasChanged();
277+
await Task.Delay(10); // Allow UI to render incrementally
278+
}
279+
}
280+
else
281+
{
282+
var response = await AgentService.InvokeAgentAsync(input, session.Id, _cts.Token);
283+
assistantMessage.Content = response;
284+
}
265285
}
266286
catch (OperationCanceledException)
267287
{

sampleapps/ChatBotUI/Components/Pages/Home.razor.css

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,59 @@
380380
font-size: 24px;
381381
}
382382
}
383+
384+
/* Streaming toggle */
385+
.streaming-toggle {
386+
display: flex;
387+
align-items: center;
388+
gap: 8px;
389+
margin-left: auto;
390+
}
391+
392+
.toggle-label {
393+
position: relative;
394+
display: inline-block;
395+
width: 36px;
396+
height: 20px;
397+
cursor: pointer;
398+
}
399+
400+
.toggle-label input {
401+
opacity: 0;
402+
width: 0;
403+
height: 0;
404+
}
405+
406+
.toggle-slider {
407+
position: absolute;
408+
inset: 0;
409+
background-color: var(--border-primary);
410+
border-radius: 20px;
411+
transition: background-color 0.2s;
412+
}
413+
414+
.toggle-slider::before {
415+
content: "";
416+
position: absolute;
417+
height: 14px;
418+
width: 14px;
419+
left: 3px;
420+
bottom: 3px;
421+
background-color: white;
422+
border-radius: 50%;
423+
transition: transform 0.2s;
424+
}
425+
426+
.toggle-label input:checked + .toggle-slider {
427+
background-color: var(--accent);
428+
}
429+
430+
.toggle-label input:checked + .toggle-slider::before {
431+
transform: translateX(16px);
432+
}
433+
434+
.toggle-text {
435+
font-size: 12px;
436+
color: var(--text-secondary);
437+
white-space: nowrap;
438+
}

sampleapps/ChatBotUI/Models/AgentCoreSettings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ namespace ChatBotUI.Models;
66
public class AgentCoreSettings
77
{
88
public string RuntimeArn { get; set; } = string.Empty;
9+
public string StreamingRuntimeArn { get; set; } = string.Empty;
910
public string Region { get; set; } = "us-west-2";
1011
}

sampleapps/ChatBotUI/Services/AgentCoreService.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,93 @@ public async Task<string> InvokeAgentAsync(string prompt, string? sessionId = nu
8181
}
8282
}
8383

84+
/// <summary>
85+
/// Invokes the AgentCore Runtime streaming agent and yields response chunks as they arrive via SSE.
86+
/// </summary>
87+
public async IAsyncEnumerable<string> InvokeAgentStreamingAsync(
88+
string prompt, string? sessionId = null,
89+
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
90+
{
91+
var arn = _settings.StreamingRuntimeArn;
92+
if (string.IsNullOrEmpty(arn))
93+
{
94+
yield return "Error: StreamingRuntimeArn is not configured in appsettings.json";
95+
yield break;
96+
}
97+
98+
_logger.LogInformation("Invoking streaming AgentCore Runtime: {Arn}", arn);
99+
100+
var payload = JsonSerializer.Serialize(new { prompt });
101+
var payloadBytes = Encoding.UTF8.GetBytes(payload);
102+
103+
var request = new InvokeAgentRuntimeRequest
104+
{
105+
AgentRuntimeArn = arn,
106+
Payload = new MemoryStream(payloadBytes),
107+
ContentType = "application/json",
108+
Accept = "text/event-stream",
109+
};
110+
111+
if (!string.IsNullOrEmpty(sessionId))
112+
{
113+
request.RuntimeSessionId = sessionId;
114+
}
115+
116+
InvokeAgentRuntimeResponse? response = null;
117+
string? invokeError = null;
118+
try
119+
{
120+
response = await _client.InvokeAgentRuntimeAsync(request, cancellationToken);
121+
}
122+
catch (Exception ex)
123+
{
124+
_logger.LogError(ex, "Error invoking streaming AgentCore Runtime");
125+
invokeError = $"Error: {ex.Message}";
126+
}
127+
128+
if (invokeError is not null)
129+
{
130+
yield return invokeError;
131+
yield break;
132+
}
133+
134+
using var reader = new StreamReader(response!.Response);
135+
while (true)
136+
{
137+
cancellationToken.ThrowIfCancellationRequested();
138+
139+
var line = await reader.ReadLineAsync(cancellationToken);
140+
if (line is null) break;
141+
if (!line.StartsWith("data: ")) continue;
142+
143+
var json = line["data: ".Length..];
144+
var chunk = ParseSseChunk(json);
145+
146+
if (chunk is null) break; // "done" event
147+
if (chunk.Length > 0) yield return chunk;
148+
}
149+
}
150+
151+
/// <summary>
152+
/// Parses an SSE data payload. Returns the chunk text, empty string for skip, or null for "done".
153+
/// </summary>
154+
private string? ParseSseChunk(string json)
155+
{
156+
try
157+
{
158+
using var doc = JsonDocument.Parse(json);
159+
160+
if (doc.RootElement.TryGetProperty("done", out var doneProp) && doneProp.GetBoolean())
161+
return null;
162+
163+
if (doc.RootElement.TryGetProperty("chunk", out var chunkProp))
164+
return chunkProp.GetString() ?? string.Empty;
165+
166+
return string.Empty;
167+
}
168+
catch (JsonException)
169+
{
170+
return string.Empty;
171+
}
172+
}
84173
}

sampleapps/ChatBotUI/appsettings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"AllowedHosts": "*",
99
"AgentCore": {
1010
"RuntimeArn": "<AgentCoreRuntimeArn>",
11+
"StreamingRuntimeArn": "<StreamingAgentCoreRuntimeArn>",
1112
"Region": "us-west-2"
1213
}
1314
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
2+
USER $APP_UID
3+
WORKDIR /app
4+
EXPOSE 8080
5+
EXPOSE 8081
6+
7+
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
8+
ARG BUILD_CONFIGURATION=Release
9+
WORKDIR /src
10+
COPY ["sampleapps/StreamingAgent/StreamingAgent.csproj", "sampleapps/StreamingAgent/"]
11+
COPY ["src/AWS.AgentCore/AWS.AgentCore.csproj", "src/AWS.AgentCore/"]
12+
COPY ["src/AWS.AgentCore.SourceGenerator/AWS.AgentCore.SourceGenerator.csproj", "src/AWS.AgentCore.SourceGenerator/"]
13+
RUN dotnet restore "sampleapps/StreamingAgent/StreamingAgent.csproj"
14+
COPY . .
15+
WORKDIR "/src/sampleapps/StreamingAgent"
16+
RUN dotnet build "./StreamingAgent.csproj" -c $BUILD_CONFIGURATION -o /app/build
17+
18+
FROM build AS publish
19+
ARG BUILD_CONFIGURATION=Release
20+
RUN dotnet publish "./StreamingAgent.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
21+
22+
FROM base AS final
23+
WORKDIR /app
24+
COPY --from=publish /app/publish .
25+
ENTRYPOINT ["dotnet", "StreamingAgent.dll"]
26+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
namespace StreamingAgent.Models;
5+
6+
public record PromptRequest(string? Prompt);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.ComponentModel;
5+
using System.Runtime.CompilerServices;
6+
using AWS.AgentCore;
7+
using AWS.AgentCore.Extensions;
8+
using Microsoft.Extensions.AI;
9+
using StreamingAgent.Models;
10+
11+
var builder = WebApplication.CreateBuilder(args);
12+
13+
builder.AddAgentCore(options =>
14+
{
15+
options.ModelId = "global.anthropic.claude-sonnet-4-20250514-v1:0";
16+
});
17+
18+
var app = builder.Build();
19+
20+
app.MapAgentCore<PromptRequest>(
21+
(PromptRequest request, AgentCoreRuntimeContext context, IChatClient chatClient,
22+
ILogger<Program> logger, CancellationToken cancellationToken) =>
23+
{
24+
logger.LogInformation("Streaming invocation — SessionId={SessionId}, RequestId={RequestId}",
25+
context.SessionId, context.RequestId);
26+
27+
return Stream();
28+
29+
async IAsyncEnumerable<string> Stream([EnumeratorCancellation] CancellationToken ct = default)
30+
{
31+
var agent = chatClient.AsAIAgent(tools: [AIFunctionFactory.Create(GetWeather)]);
32+
var session = await agent.CreateSessionAsync(cancellationToken: cancellationToken);
33+
34+
await foreach (var update in agent.RunStreamingAsync(
35+
request.Prompt ?? "Hello!", session, cancellationToken: cancellationToken))
36+
{
37+
var text = update.Text;
38+
if (!string.IsNullOrEmpty(text))
39+
yield return text;
40+
}
41+
42+
logger.LogInformation("Streaming complete — SessionId={SessionId}, RequestId={RequestId}",
43+
context.SessionId, context.RequestId);
44+
}
45+
});
46+
47+
app.Run();
48+
49+
[Description("Gets the current weather for a given location.")]
50+
static string GetWeather([Description("The city or location to get weather for.")] string location)
51+
=> $"The current weather in {location} is 72°F and sunny.";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<ProjectReference Include="..\..\src\AWS.AgentCore\AWS.AgentCore.csproj" />
11+
</ItemGroup>
12+
13+
</Project>

0 commit comments

Comments
 (0)