Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
foundry.AsExisting(existingFoundryName, existingFoundryResourceGroup);

// Add the writer agent service
var writerAgent = builder.AddProject<Projects.WriterAgent>("writer-agent")
.WithHttpHealthCheck("/health")
var writerAgent = builder.AddProject<Projects.WriterAgent>("writer-agent", launchProfileName: "https")
.WithHttpHealthCheck("/health", endpointName: "https")
.WithReference(foundry).WaitFor(foundry);

// Add the editor agent service
Expand Down
2 changes: 2 additions & 0 deletions dotnet/samples/05-end-to-end/DevUIAspireIntegration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ The solution contains two agent services:
- **WriterAgent** — a simple agent that writes short stories (≤ 300 words) about a given topic.
- **EditorAgent** — an agent that edits stories for grammar and style, selects a title, and formats the result for publishing. It also demonstrates tool use via `AIFunctionFactory`.

The WriterAgent is configured with HTTPS redirection so the Aspire DevUI integration can be tested against an agent service that exposes both HTTP and HTTPS endpoints.

## Prerequisites

- [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

var app = builder.Build();

app.UseHttpsRedirection();

// Map OpenAI API endpoints — DevUI aggregator routes requests here
app.MapOpenAIResponses();
app.MapOpenAIConversations();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7280;http://localhost:5280",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ namespace Aspire.Hosting.AgentFramework;
internal sealed class DevUIAggregatorHostedService : IAsyncDisposable
{
private static readonly FileExtensionContentTypeProvider s_contentTypeProvider = new();
private static readonly string[] s_preferredBackendEndpointNames = ["https", "http"];

private WebApplication? _app;
private readonly DevUIResource _resource;
Expand Down Expand Up @@ -283,7 +284,7 @@ private void MapRoutes(WebApplication app)
/// Resolves backend URLs from the resource's <see cref="AgentServiceAnnotation"/> annotations.
/// This method does not cache results to ensure late-allocated backends are always discovered.
/// </summary>
private Dictionary<string, string> ResolveBackends()
internal Dictionary<string, string> ResolveBackends()
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);

Expand All @@ -296,21 +297,39 @@ private Dictionary<string, string> ResolveBackends()

var prefix = annotation.EntityIdPrefix ?? annotation.AgentService.Name;

var endpointUrl = this.ResolveBackendUrl(rwe, prefix);
if (endpointUrl is not null)
{
result[prefix] = endpointUrl;
}
}

return result;
}

private string? ResolveBackendUrl(IResourceWithEndpoints resource, string prefix)
{
foreach (var endpointName in s_preferredBackendEndpointNames)
{
try
{
var endpoint = rwe.GetEndpoint("http");
var endpoint = resource.GetEndpoint(endpointName);
if (endpoint.IsAllocated)
{
result[prefix] = endpoint.Url;
return endpoint.Url;
}
}
catch (Exception ex)
catch (InvalidOperationException ex)
{
this._logger.LogDebug(ex, "Backend '{Prefix}' endpoint not yet available", prefix);
this._logger.LogDebug(
ex,
"Backend '{Prefix}' {EndpointName} endpoint not yet available",
prefix,
endpointName);
}
}

return result;
return null;
}

private async Task<IResult> AggregateEntitiesAsync(HttpContext context)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Linq;
using System.Net.Sockets;
using Aspire.Hosting.ApplicationModel;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;

namespace Aspire.Hosting.AgentFramework.DevUI.UnitTests;

Expand Down Expand Up @@ -245,6 +247,72 @@ public void WithAgentService_MultipleServices_CreatesMultipleAnnotations()

#endregion

#region Backend Endpoint Selection Tests

/// <summary>
/// Verifies that ResolveBackends prefers the HTTPS endpoint when both HTTP and HTTPS are allocated.
/// </summary>
[Fact]
public void ResolveBackends_WithHttpAndHttpsEndpoints_PrefersHttps()
{
// Arrange
var devui = new DevUIResource("devui");
var agentService = new TestEndpointResource("writer-agent");
AddAllocatedEndpoint(agentService, "http", "http", 5050);
AddAllocatedEndpoint(agentService, "https", "https", 7443);
devui.Annotations.Add(new AgentServiceAnnotation(agentService));
var aggregator = new DevUIAggregatorHostedService(devui, NullLogger.Instance);

// Act
var backends = aggregator.ResolveBackends();

// Assert
Assert.Equal("https://localhost:7443", backends["writer-agent"]);
}

/// <summary>
/// Verifies that ResolveBackends falls back to HTTP when the HTTPS endpoint is not present.
/// </summary>
[Fact]
public void ResolveBackends_WithOnlyHttpEndpoint_UsesHttp()
{
// Arrange
var devui = new DevUIResource("devui");
var agentService = new TestEndpointResource("writer-agent");
AddAllocatedEndpoint(agentService, "http", "http", 5050);
devui.Annotations.Add(new AgentServiceAnnotation(agentService));
var aggregator = new DevUIAggregatorHostedService(devui, NullLogger.Instance);

// Act
var backends = aggregator.ResolveBackends();

// Assert
Assert.Equal("http://localhost:5050", backends["writer-agent"]);
}

/// <summary>
/// Verifies that ResolveBackends falls back to HTTP when the HTTPS endpoint has not been allocated yet.
/// </summary>
[Fact]
public void ResolveBackends_WithUnallocatedHttpsEndpoint_UsesHttp()
{
// Arrange
var devui = new DevUIResource("devui");
var agentService = new TestEndpointResource("writer-agent");
AddEndpoint(agentService, "https", "https");
AddAllocatedEndpoint(agentService, "http", "http", 5050);
devui.Annotations.Add(new AgentServiceAnnotation(agentService));
var aggregator = new DevUIAggregatorHostedService(devui, NullLogger.Instance);

// Act
var backends = aggregator.ResolveBackends();

// Assert
Assert.Equal("http://localhost:5050", backends["writer-agent"]);
}

#endregion

#region Entity ID Parsing Tests

/// <summary>
Expand Down Expand Up @@ -294,5 +362,33 @@ private static IResourceBuilder<IResourceWithEndpoints> CreateMockAgentServiceBu
return mockBuilder.Object;
}

private static void AddAllocatedEndpoint(
TestEndpointResource resource,
string name,
string uriScheme,
int port)
{
var endpoint = AddEndpoint(resource, name, uriScheme);
endpoint.AllocatedEndpoint = new AllocatedEndpoint(endpoint, "localhost", port);
}

private static EndpointAnnotation AddEndpoint(
TestEndpointResource resource,
string name,
string uriScheme)
{
var endpoint = new EndpointAnnotation(
ProtocolType.Tcp,
uriScheme: uriScheme,
name: name,
port: null,
isProxied: false);

resource.Annotations.Add(endpoint);
return endpoint;
}

private sealed class TestEndpointResource(string name) : Resource(name), IResourceWithEndpoints;

#endregion
}
Loading