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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,6 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp

# Backup files
*.original.cs
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "src/a2a-dotnet"]
path = src/a2a-dotnet
url = https://github.com/a2aproject/a2a-dotnet
39 changes: 31 additions & 8 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,47 @@
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.4.1" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.4.2" />
<PackageVersion Include="Aspire.Hosting.Redis" Version="9.4.2" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.7.25380.108" />

<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.8.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="9.4.1" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.A2A" Version="1.63.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.63.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.A2A" Version="1.64.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.64.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.63.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureOpenAI" Version="1.63.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.63.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureOpenAI" Version="1.64.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.64.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Redis" Version="1.64.0-preview" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0-preview.7.25380.108" />
<PackageVersion Include="Microsoft.Identity.Web" Version="3.13.2" />
<PackageVersion Include="Microsoft.Identity.Web" Version="3.14.0" />
<PackageVersion Include="Scalar.AspNetCore" Version="2.7.2" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
<PackageVersion Include="A2A.AspNetCore" Version="0.1.0-preview.2" />
<PackageVersion Include="ModelContextProtocol" Version="0.3.0-preview.3" />
<PackageVersion Include="A2A.AspNetCore" Version="0.3.1-preview" />
<PackageVersion Include="ModelContextProtocol" Version="0.3.0-preview.4" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="0.3.0-preview.4" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.64.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
</ItemGroup>
<ItemGroup Label="Web">
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.8.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="Microsoft.SemanticKernel.Core" Version="1.61.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.SqliteVec" Version="1.61.0-preview" />
</ItemGroup>
<ItemGroup Label="Testing">
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0-preview.7.25380.15" />
<PackageVersion Include="xunit" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0-preview.7.25380.108" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.4.2" />
<PackageVersion Include="Microsoft.Extensions.Testing.Abstractions" Version="9.8.0" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion app.http
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Content-Type: application/json
"parts": [
{
"kind": "text",
"text": "I want a black coffee, cappuccino and a cake pop."
"text": "I want a black coffee, cappuccino and 2 cake pops."
}
]
}
Expand Down
68 changes: 68 additions & 0 deletions build-helper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/bin/bash

# Function to temporarily change target frameworks for building
change_target_frameworks() {
echo "Temporarily changing target frameworks to net8.0 for building..."

# Change all project files to use net8.0
find src -name "*.csproj" -exec sed -i 's/<TargetFramework>net10.0<\/TargetFramework>/<TargetFramework>net8.0<\/TargetFramework>/g' {} \;
find tests -name "*.csproj" -exec sed -i 's/<TargetFramework>net10.0<\/TargetFramework>/<TargetFramework>net8.0<\/TargetFramework>/g' {} \;

# Update global.json to use .NET 8.0
sed -i 's/"version": "10.0.100-preview.7.25380.108"/"version": "8.0.119"/g' global.json

# Update packages to compatible versions for .NET 8.0
sed -i 's/Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0-preview.7.25380.108"/Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8"/g' Directory.Packages.props
sed -i 's/"Microsoft.NET.Test.Sdk" Version="18.0.0-preview.7.25380.15"/"Microsoft.NET.Test.Sdk" Version="17.10.0"/g' Directory.Packages.props
sed -i 's/"Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0-preview.7.25380.108"/"Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8"/g' Directory.Packages.props
sed -i 's/"Microsoft.Extensions.Testing.Abstractions" Version="9.8.0"/"Microsoft.Extensions.Testing.Abstractions" Version="8.8.0"/g' Directory.Packages.props
sed -i 's/"Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.7.25380.108"/"Microsoft.AspNetCore.OpenApi" Version="8.0.8"/g' Directory.Packages.props
sed -i 's/"Scalar.AspNetCore" Version="2.7.2"/"Scalar.AspNetCore" Version="1.2.20"/g' Directory.Packages.props
}

# Function to restore target frameworks back to net10.0
restore_target_frameworks() {
echo "Restoring target frameworks to net10.0..."

# Restore all project files to use net10.0
find src -name "*.csproj" -exec sed -i 's/<TargetFramework>net8.0<\/TargetFramework>/<TargetFramework>net10.0<\/TargetFramework>/g' {} \;
find tests -name "*.csproj" -exec sed -i 's/<TargetFramework>net8.0<\/TargetFramework>/<TargetFramework>net10.0<\/TargetFramework>/g' {} \;

# Restore package versions
sed -i 's/Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8"/Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0-preview.7.25380.108"/g' Directory.Packages.props
sed -i 's/"Microsoft.NET.Test.Sdk" Version="17.10.0"/"Microsoft.NET.Test.Sdk" Version="18.0.0-preview.7.25380.15"/g' Directory.Packages.props
sed -i 's/"Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8"/"Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0-preview.7.25380.108"/g' Directory.Packages.props
sed -i 's/"Microsoft.Extensions.Testing.Abstractions" Version="8.8.0"/"Microsoft.Extensions.Testing.Abstractions" Version="9.8.0"/g' Directory.Packages.props
sed -i 's/"Microsoft.AspNetCore.OpenApi" Version="8.0.8"/"Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.7.25380.108"/g' Directory.Packages.props
sed -i 's/"Scalar.AspNetCore" Version="1.2.20"/"Scalar.AspNetCore" Version="2.7.2"/g' Directory.Packages.props

# Restore global.json
sed -i 's/"version": "8.0.119"/"version": "10.0.100-preview.7.25380.108"/g' global.json
}

if [ "$1" = "build" ]; then
change_target_frameworks
echo "Configuration changed to .NET 8.0. You can now run 'dotnet build' and 'dotnet test'."
echo "Remember to run '$0 restore' when done to restore .NET 10.0 settings."
elif [ "$1" = "restore" ]; then
restore_target_frameworks
echo "Configuration restored to .NET 10.0."
elif [ "$1" = "quick-test" ]; then
change_target_frameworks
dotnet build
build_result=$?
if [ $build_result -eq 0 ]; then
echo "Build successful, running tests..."
dotnet test --verbosity normal
test_result=$?
else
test_result=$build_result
fi
restore_target_frameworks
exit $test_result
else
echo "Usage: $0 [build|restore|quick-test]"
echo " build - Change to .NET 8.0 for building"
echo " restore - Restore to .NET 10.0"
echo " quick-test - Build and test with .NET 8.0, then restore .NET 10.0"
fi
25 changes: 20 additions & 5 deletions coffeeshop_agent.slnx
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
<Solution>
<Folder Name="/Solution Items/">
<Folder Name="/AgentGateway/">
<File Path="agentgateway/config.yaml" />
</Folder>
<Folder Name="/Agents/">
<Project Path="src/BaristaService/BaristaService.csproj" Id="9a445033-1195-4525-b759-c6d327db8eed" />
<Project Path="src/CounterService/CounterService.csproj" Id="8fdefb0e-a8c9-47be-9598-17bac58a4d25" />
<Project Path="src/KitchenService/KitchenService.csproj" Id="592d052d-5a1b-448d-b94e-51481d33fbd7" />
</Folder>
<Folder Name="/MCPServers/">
<Project Path="src/ProductCatalogService/ProductCatalogService.csproj" Id="06976d3a-1bcf-4fcc-8131-43ea53d95298" />
</Folder>
<Folder Name="/Web/">
<Project Path="src/ChatApp/ChatApp.csproj" />
</Folder>
<Folder Name="/__Solution Items/">
<File Path=".editorconfig" />
<File Path=".gitignore" />
<File Path="app.http" />
Expand All @@ -9,10 +23,11 @@
<File Path="nuget.config" />
<File Path="README.md" />
</Folder>
<Folder Name="/__Tests/">
<Project Path="tests/ServiceDefaults.Tests/ServiceDefaults.Tests.csproj" Id="12345678-1234-5678-9abc-def012345678" />
<Project Path="tests/CounterService.Tests/CounterService.Tests.csproj" Id="87654321-4321-8765-cba9-fed087654321" />
<Project Path="tests/SamplesIntegrationTests/SamplesIntegrationTests.csproj" Id="11111111-2222-3333-4444-555555555555" />
</Folder>
<Project Path="src/AppHost/AppHost.csproj" Id="6fa56cf5-357b-4533-bc94-b440599d677c" />
<Project Path="src/BaristaService/BaristaService.csproj" Id="9a445033-1195-4525-b759-c6d327db8eed" />
<Project Path="src/CounterService/CounterService.csproj" Id="8fdefb0e-a8c9-47be-9598-17bac58a4d25" />
<Project Path="src/KitchenService/KitchenService.csproj" Id="592d052d-5a1b-448d-b94e-51481d33fbd7" />
<Project Path="src/ProductCatalogService/ProductCatalogService.csproj" Id="06976d3a-1bcf-4fcc-8131-43ea53d95298" />
<Project Path="src/ServiceDefaults/ServiceDefaults.csproj" Id="c2e4647f-53d9-4571-978d-af77987d95fa" />
</Solution>
41 changes: 36 additions & 5 deletions src/AppHost/AppHost.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,49 @@
var builder = DistributedApplication.CreateBuilder(args);

var chatModelId = builder.AddConnectionString("chatModelId");
var embeddingModelId = builder.AddConnectionString("embeddingModelId");
var endpoint = builder.AddConnectionString("endpoint");
var apiKey = builder.AddConnectionString("apiKey");

builder.AddProject<Projects.ProductCatalogService>("product-catalog");
var cache = builder.AddRedis("cache")
.WithLifetime(ContainerLifetime.Persistent)
.WithRedisInsight();

var counter = builder.AddProject<Projects.CounterService>("counter");
var product = builder.AddProject<Projects.ProductCatalogService>("product")
.WithEnvironment("AzureAd__Instance", builder.Configuration["AzureAd:Instance"])
.WithEnvironment("AzureAd__TenantId", builder.Configuration["AzureAd:TenantId"])
.WithEnvironment("AzureAd__ClientId", builder.Configuration["AzureAd:ProductClientId"]);

var barista = builder.AddProject<Projects.BaristaService>("barista")
.WithEnvironment("AzureAd__Instance", builder.Configuration["AzureAd:Instance"])
.WithEnvironment("AzureAd__TenantId", builder.Configuration["AzureAd:TenantId"])
.WithEnvironment("AzureAd__ClientId", builder.Configuration["AzureAd:BaristaClientId"]);

var kitchen = builder.AddProject<Projects.KitchenService>("kitchen")
.WithEnvironment("AzureAd__Instance", builder.Configuration["AzureAd:Instance"])
.WithEnvironment("AzureAd__TenantId", builder.Configuration["AzureAd:TenantId"])
.WithEnvironment("AzureAd__ClientId", builder.Configuration["AzureAd:KitchenClientId"]);

var counter = builder.AddProject<Projects.CounterService>("counter")
.WithEnvironment("AzureAd__Instance", builder.Configuration["AzureAd:Instance"])
.WithEnvironment("AzureAd__TenantId", builder.Configuration["AzureAd:TenantId"])
.WithEnvironment("AzureAd__ClientId", builder.Configuration["AzureAd:CounterClientId"])
.WithEnvironment("AzureAd__ClientSecret", builder.Configuration["AzureAd:CounterClientSecret"])
.WithReference(product).WaitFor(product)
.WithReference(barista).WaitFor(barista)
.WithReference(kitchen).WaitFor(kitchen);
counter.WithReference(chatModelId);
counter.WithReference(embeddingModelId);
counter.WithReference(endpoint);
counter.WithReference(apiKey);
counter.WithReference(cache).WaitFor(cache);

builder.AddProject<Projects.BaristaService>("barista");

builder.AddProject<Projects.KitchenService>("kitchen");
builder.AddProject<Projects.ChatApp>("web")
.WithEnvironment("AzureAd__Domain", builder.Configuration["AzureAd:Domain"])
.WithEnvironment("AzureAd__Instance", builder.Configuration["AzureAd:Instance"])
.WithEnvironment("AzureAd__TenantId", builder.Configuration["AzureAd:TenantId"])
.WithEnvironment("AzureAd__ClientId", builder.Configuration["AzureAd:CounterClientId"])
.WithEnvironment("AzureAd__ClientSecret", builder.Configuration["AzureAd:CounterClientSecret"])
.WithReference(counter).WaitFor(counter);

builder.Build().Run();
2 changes: 2 additions & 0 deletions src/AppHost/AppHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="Aspire.Hosting.Redis" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\BaristaService\BaristaService.csproj" />
<ProjectReference Include="..\ChatApp\ChatApp.csproj" />
<ProjectReference Include="..\CounterService\CounterService.csproj" />
<ProjectReference Include="..\KitchenService\KitchenService.csproj" />
<ProjectReference Include="..\ProductCatalogService\ProductCatalogService.csproj" />
Expand Down
127 changes: 16 additions & 111 deletions src/BaristaService/Agents/BaristaAgent.cs
Original file line number Diff line number Diff line change
@@ -1,118 +1,23 @@
using System.Diagnostics;
using A2A;
using ServiceDefaults;
using ServiceDefaults.Agents;
using Microsoft.Extensions.Logging;

namespace BaristaService.Agents;

public class BaristaAgent(IHttpContextAccessor httpContextAccessor, ILogger<BaristaAgent> logger)
/// <summary>
/// Barista agent implementation using the common SimpleAgent base class.
/// This eliminates code duplication and follows DRY principles.
/// Reference: Clean Code by Robert Martin - Chapter 17: Smells and Heuristics (G5: Duplication)
/// </summary>
public class BaristaAgent : SimpleAgent
{
private ITaskManager? _taskManager;
public IHttpContextAccessor HttpContextAccessor { get; } = httpContextAccessor;
public ILogger<BaristaAgent> Logger { get; } = logger;

public static readonly ActivitySource ActivitySource = new($"A2A.{nameof(BaristaAgent)}", "1.0.0");

public void Attach(ITaskManager taskManager)
{
_taskManager = taskManager;
_taskManager.OnTaskCreated = OnTaskCreatedAsync;
_taskManager.OnTaskUpdated = OnTaskUpdatedAsync;
_taskManager.OnAgentCardQuery = GetAgentCardAsync;
}

private async Task OnTaskCreatedAsync(AgentTask task, CancellationToken cancellationToken)
{
using var activity = ActivitySource.StartActivity("OnTaskCreated", ActivityKind.Server);
activity?.SetTag("task.id", task.Id);

Logger.LogInformation("Task created with ID: {TaskId}", task.Id);
await ProcessTaskAsync(task, cancellationToken);
}

private async Task OnTaskUpdatedAsync(AgentTask task, CancellationToken cancellationToken)
{
using var activity = ActivitySource.StartActivity("OnTaskUpdated", ActivityKind.Server);
activity?.SetTag("task.id", task.Id);

Logger.LogInformation("Task updated with ID: {TaskId}", task.Id);
await ProcessTaskAsync(task, cancellationToken);
}

private async Task ProcessTaskAsync(AgentTask task, CancellationToken cancellationToken)
public BaristaAgent(IHttpContextAccessor httpContextAccessor, ILogger<BaristaAgent> logger)
: base(
logger,
AgentConstants.ActivitySources.Barista,
httpContextAccessor,
"Barista Service Agent",
"A2A server agent that processes messages and integrates with MCP server for admin users.")
{
using var activity = ActivitySource.StartActivity("OnTaskUpdated", ActivityKind.Server);
activity?.SetTag("task.id", task.Id);

if (_taskManager == null)
{
throw new InvalidOperationException("TaskManager is not attached.");
}

try
{
// Complete the task
await _taskManager.UpdateStatusAsync(
task.Id,
TaskState.Completed,
new Message
{
Parts = [new TextPart { Text = "Message processed successfully" }]
},
final: true,
cancellationToken: cancellationToken);

Logger.LogInformation("Task {TaskId} completed successfully", task.Id);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error processing task {TaskId}", task.Id);

await _taskManager.UpdateStatusAsync(
task.Id,
TaskState.Failed,
new Message
{
Parts = [new TextPart { Text = $"Error processing ping message: {ex.Message}" }]
},
final: true,
cancellationToken: cancellationToken);
}
}

private Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<AgentCard>(cancellationToken);
}

var capabilities = new AgentCapabilities
{
Streaming = true,
PushNotifications = false,
};

// Note: Authentication is implemented at the HTTP transport level using Microsoft Entra ID
// JWT Bearer tokens are required for all endpoints and are validated by the middleware
// The authentication scheme used is "Bearer" with JWT tokens containing required scopes
return Task.FromResult(new AgentCard
{
Name = "Barista Service Agent",
Description = "A2A server agent that processes messages and integrates with MCP server for admin users. " +
"AUTHENTICATION REQUIRED: This agent requires Microsoft Entra ID JWT Bearer token authentication " +
"with 'access_as_user' scope. All requests must include valid JWT tokens in the Authorization header.",
Url = agentUrl,
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [
new AgentSkill
{
Name = "process_order",
Description = "Process messages and communicate with MCP server for admin users. " +
"Requires JWT authentication with admin role and 'access_as_user' scope."
}
],
});
}
}
1 change: 1 addition & 0 deletions src/BaristaService/BaristaService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>7a17e8b0-9312-4cba-b3a8-b617317acce9</UserSecretsId>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading