+
diff --git a/src/ChatApp/ChatApp.csproj b/src/ChatApp/ChatApp.csproj
new file mode 100644
index 0000000..3f029ac
--- /dev/null
+++ b/src/ChatApp/ChatApp.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net10.0
+ enable
+ enable
+ f2fe61b0-2fc1-4560-820c-41bc975cb1d2
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ChatApp/Components/App.razor b/src/ChatApp/Components/App.razor
new file mode 100644
index 0000000..57a625f
--- /dev/null
+++ b/src/ChatApp/Components/App.razor
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code {
+ private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false);
+}
\ No newline at end of file
diff --git a/src/ChatApp/Components/Layout/MainLayout.razor b/src/ChatApp/Components/Layout/MainLayout.razor
new file mode 100644
index 0000000..54a6694
--- /dev/null
+++ b/src/ChatApp/Components/Layout/MainLayout.razor
@@ -0,0 +1,7 @@
+@inherits LayoutComponentBase
+
+
+
+ @Body
+
+
\ No newline at end of file
diff --git a/src/ChatApp/Components/Pages/Home.razor b/src/ChatApp/Components/Pages/Home.razor
new file mode 100644
index 0000000..bdf4f35
--- /dev/null
+++ b/src/ChatApp/Components/Pages/Home.razor
@@ -0,0 +1,7 @@
+@page "/"
+
+Coffee Shop Agent
+
+Welcome to Coffee Shop Agent!
+
+This is a Blazor web app for interacting with the Coffee Shop Agent system.
\ No newline at end of file
diff --git a/src/ChatApp/Components/Routes.razor b/src/ChatApp/Components/Routes.razor
new file mode 100644
index 0000000..8eab673
--- /dev/null
+++ b/src/ChatApp/Components/Routes.razor
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ChatApp/Components/_Imports.razor b/src/ChatApp/Components/_Imports.razor
new file mode 100644
index 0000000..6cde179
--- /dev/null
+++ b/src/ChatApp/Components/_Imports.razor
@@ -0,0 +1,12 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.Extensions.AI
+@using Microsoft.JSInterop
+@using ChatApp
+@using ChatApp.Components
+@using ChatApp.Components.Layout
\ No newline at end of file
diff --git a/src/ChatApp/Program.cs b/src/ChatApp/Program.cs
new file mode 100644
index 0000000..cd5e6f2
--- /dev/null
+++ b/src/ChatApp/Program.cs
@@ -0,0 +1,134 @@
+using System.Net.Http.Headers;
+using ChatApp.Components;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Identity.Web;
+
+// Ref: https://github.com/damienbod/BlazorServerOidc/tree/main/BlazorWebApp
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddRazorComponents().AddInteractiveServerComponents();
+
+builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
+ .AddMicrosoftIdentityWebApp(options =>
+ {
+ builder.Configuration.Bind("AzureAd", options);
+
+ options.SaveTokens = true;
+ options.Scope.Add($"api://{builder.Configuration["AzureAd:ClientId"]}/CoffeeShop.Counter.ReadWrite");
+
+ options.Events = new OpenIdConnectEvents
+ {
+ OnTokenValidated = CustomTokenValidated,
+ OnAuthenticationFailed = CustomAuthenticationFailed
+ };
+
+ }, options => builder.Configuration.Bind("AzureAd", options));
+
+builder.Services.AddHttpContextAccessor();
+
+builder.Services.AddScoped();
+
+builder.Services.AddHttpClient("CounterClient",
+ client => client.BaseAddress = new Uri("https+http://counter" ??
+ throw new Exception("Missing base address!")))
+ .AddHttpMessageHandler();
+
+builder.Services.AddAuthenticationCore();
+builder.Services.AddAuthorization();
+builder.Services.AddCascadingAuthenticationState();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (!app.Environment.IsDevelopment())
+{
+ app.UseExceptionHandler("/Error", createScopeForErrors: true);
+ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+ app.UseHsts();
+}
+
+app.UseHttpsRedirection();
+app.UseAntiforgery();
+
+app.UseAuthentication();
+app.UseAuthorization();
+
+app.UseStaticFiles();
+app.MapRazorComponents()
+ .AddInteractiveServerRenderMode();
+
+app.MapLoginLogoutEndpoints();
+
+app.Run();
+
+async Task CustomTokenValidated(TokenValidatedContext context)
+{
+ await Task.CompletedTask;
+}
+
+async Task CustomAuthenticationFailed(AuthenticationFailedContext context)
+{
+ // Custom logic upon authentication failure
+ await Task.CompletedTask;
+}
+
+public class TokenHandler(IHttpContextAccessor httpContextAccessor) :
+ DelegatingHandler
+{
+ protected override async Task SendAsync(
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ if (httpContextAccessor.HttpContext is null)
+ {
+ throw new Exception("HttpContext not available");
+ }
+
+ var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
+
+ if (accessToken is null)
+ {
+ throw new Exception("No access token");
+ }
+
+ request.Headers.Authorization =
+ new AuthenticationHeaderValue("Bearer", accessToken);
+
+ return await base.SendAsync(request, cancellationToken);
+ }
+}
+
+public static class LoginLogoutEndpoints
+{
+ public static WebApplication MapLoginLogoutEndpoints(this WebApplication app)
+ {
+ app.MapGet("/login", async context =>
+ {
+ var returnUrl = context.Request.Query["returnUrl"];
+
+ await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties
+ {
+ RedirectUri = returnUrl == StringValues.Empty ? "/" : returnUrl.ToString()
+ });
+ }).AllowAnonymous();
+
+ app.MapPost("/logout", async context =>
+ {
+ if (context.User.Identity?.IsAuthenticated ?? false)
+ {
+ await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
+ await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
+ }
+ else
+ {
+ context.Response.Redirect("/");
+ }
+ });
+
+ return app;
+ }
+
+}
\ No newline at end of file
diff --git a/src/ChatApp/appsettings.Development.json b/src/ChatApp/appsettings.Development.json
new file mode 100644
index 0000000..b6f634e
--- /dev/null
+++ b/src/ChatApp/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "DetailedErrors": true,
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ChatApp/appsettings.json b/src/ChatApp/appsettings.json
new file mode 100644
index 0000000..ec04bc1
--- /dev/null
+++ b/src/ChatApp/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
\ No newline at end of file
diff --git a/src/CounterService/Agents/CounterAgent.cs b/src/CounterService/Agents/CounterAgent.cs
index da477b5..c4b2405 100644
--- a/src/CounterService/Agents/CounterAgent.cs
+++ b/src/CounterService/Agents/CounterAgent.cs
@@ -90,7 +90,7 @@ protected override async Task ProcessTaskCoreAsync(AgentTask task, CancellationT
await _taskManager!.UpdateStatusAsync(
task.Id,
TaskState.Failed,
- new Message { Parts = [new TextPart { Text = validationResult.ErrorMessage! }] },
+ new AgentMessage { Parts = [new TextPart { Text = validationResult.ErrorMessage! }] },
final: true,
cancellationToken: cancellationToken);
return;
@@ -102,7 +102,7 @@ protected override async Task ProcessTaskCoreAsync(AgentTask task, CancellationT
await _taskManager!.UpdateStatusAsync(
task.Id,
TaskState.Working,
- new Message
+ new AgentMessage
{
Parts = [new TextPart { Text = $"Processing order via A2A protocol: {messageText}" }]
},
@@ -125,7 +125,7 @@ protected override async Task ProcessTaskCoreAsync(AgentTask task, CancellationT
await _taskManager.UpdateStatusAsync(
task.Id,
TaskState.Completed,
- new Message
+ new AgentMessage
{
Parts = [new TextPart { Text = "Order processed successfully via A2A protocol" }]
},
diff --git a/src/ServiceDefaults/Agents/BaseAgent.cs b/src/ServiceDefaults/Agents/BaseAgent.cs
index aed59c0..1f26560 100644
--- a/src/ServiceDefaults/Agents/BaseAgent.cs
+++ b/src/ServiceDefaults/Agents/BaseAgent.cs
@@ -120,7 +120,7 @@ protected virtual async Task ValidateAuthenticationAsync(A
await _taskManager!.UpdateStatusAsync(
task.Id,
TaskState.AuthRequired,
- new Message
+ new AgentMessage
{
Parts = [new TextPart { Text = AgentConstants.ErrorMessages.UserNotAuthenticated }]
},
@@ -143,7 +143,7 @@ protected virtual async Task ValidateAuthenticationAsync(A
await _taskManager!.UpdateStatusAsync(
task.Id,
TaskState.AuthRequired,
- new Message
+ new AgentMessage
{
Parts = [new TextPart { Text = $"Missing authentication information - JWT token: {(jwtToken != null ? "present" : "missing")}, User email: {(userEmail != null ? "present" : "missing")}" }]
},
@@ -182,7 +182,7 @@ protected virtual async Task HandleTaskErrorAsync(AgentTask task, Exception ex,
await _taskManager!.UpdateStatusAsync(
task.Id,
TaskState.Failed,
- new Message
+ new AgentMessage
{
Parts = [new TextPart { Text = errorMessage }]
},
diff --git a/src/ServiceDefaults/Agents/SimpleAgent.cs b/src/ServiceDefaults/Agents/SimpleAgent.cs
index 9202ac6..0ff13ca 100644
--- a/src/ServiceDefaults/Agents/SimpleAgent.cs
+++ b/src/ServiceDefaults/Agents/SimpleAgent.cs
@@ -42,7 +42,7 @@ protected override async Task ProcessTaskCoreAsync(AgentTask task, CancellationT
await _taskManager!.UpdateStatusAsync(
task.Id,
TaskState.Completed,
- new Message
+ new AgentMessage
{
Parts = [new TextPart { Text = "Message processed successfully" }]
},
diff --git a/src/ServiceDefaults/Services/A2AMessageService.cs b/src/ServiceDefaults/Services/A2AMessageService.cs
index 2156808..6d126e4 100644
--- a/src/ServiceDefaults/Services/A2AMessageService.cs
+++ b/src/ServiceDefaults/Services/A2AMessageService.cs
@@ -116,7 +116,7 @@ private async Task SendMessageToClient(A2AClient client, str
activity?.SetTag("items.count", items.Count);
// Create A2A message with minimal metadata (authentication is in HTTP headers)
- var a2aMessage = new Message
+ var a2aMessage = new AgentMessage
{
Role = MessageRole.User,
MessageId = Guid.NewGuid().ToString(),
diff --git a/src/ServiceDefaults/Services/A2AResponseMapper.cs b/src/ServiceDefaults/Services/A2AResponseMapper.cs
index 0ccf852..4c91545 100644
--- a/src/ServiceDefaults/Services/A2AResponseMapper.cs
+++ b/src/ServiceDefaults/Services/A2AResponseMapper.cs
@@ -35,7 +35,7 @@ public A2AServiceResponse MapResponse(A2AResponse response)
return response switch
{
AgentTask task => MapTaskResponse(task),
- Message messageResponse => MapMessageResponse(messageResponse),
+ AgentMessage messageResponse => MapMessageResponse(messageResponse),
_ => MapUnknownResponse(response)
};
}
@@ -58,7 +58,7 @@ private A2AServiceResponse MapTaskResponse(AgentTask task)
};
}
- private A2AServiceResponse MapMessageResponse(Message messageResponse)
+ private A2AServiceResponse MapMessageResponse(AgentMessage messageResponse)
{
_logger.LogInformation("Received A2A message response");