Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3c95291
adds devui integration and samples
tommasodotNET Feb 9, 2026
2c23d03
adds unit tests for devui integration
tommasodotNET Feb 9, 2026
b33799d
fix: correct formatting of copyright notice in unit test files
tommasodotNET Feb 9, 2026
1da4302
Merge branch 'main' into features/3768-devui-aspire-integration
tommasodotNET Feb 10, 2026
500f1d1
fixes formatting issues
tommasodotNET Feb 10, 2026
8c7c649
fixes build for net8 target
tommasodotNET Feb 10, 2026
cd3dd0b
Merge branch 'features/3768-devui-aspire-integration' of github.com:t…
tommasodotNET Feb 10, 2026
6672805
fixes formatting errors on test apphost
tommasodotNET Feb 10, 2026
534efa8
adds copyright notice to multiple files and removes unnecessary using…
tommasodotNET Feb 10, 2026
caab54f
Merge branch 'main' into features/3768-devui-aspire-integration
tommasodotNET Feb 10, 2026
aef3514
Update dotnet/aspire-integration/Aspire.Hosting.AgentFramework.DevUI/…
tommasodotNET Feb 10, 2026
cb1b0f2
Update dotnet/aspire-integration/Aspire.Hosting.AgentFramework.DevUI/…
tommasodotNET Feb 10, 2026
669462b
Update dotnet/tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/Asp…
tommasodotNET Feb 10, 2026
b192b39
Update dotnet/samples/DevUIIntegration/DevUIIntegration.AppHost/DevUI…
tommasodotNET Feb 10, 2026
78e3ad6
Update dotnet/aspire-integration/Aspire.Hosting.AgentFramework.DevUI/…
tommasodotNET Feb 10, 2026
261fc69
Refactor project files to use TargetFrameworks instead of TargetFrame…
tommasodotNET Feb 10, 2026
36c8600
Add unit tests for DevUIAggregatorHostedService; refactor project fil…
tommasodotNET Feb 10, 2026
0a891ca
Refactor project files to use TargetFrameworks for multi-targeting su…
tommasodotNET Feb 10, 2026
05a3340
Remove unnecessary using directive for Aspire.Hosting in DevUIAggrega…
tommasodotNET Feb 10, 2026
f38fbdf
Merge branch 'main' into features/3768-devui-aspire-integration
tommasodotNET Feb 13, 2026
daba7a6
Merge branch 'main' into features/3768-devui-aspire-integration
tommasodotNET Feb 17, 2026
48efb26
Merge branch 'main' into features/3768-devui-aspire-integration
tommasodotNET Mar 5, 2026
fedbc3b
merge
tommasodotNET Mar 5, 2026
d31e5b8
fixes Conversation routing for non-first backends
tommasodotNET Mar 5, 2026
0dfe1d5
add documentation for devui integration sample
tommasodotNET Mar 5, 2026
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
35 changes: 19 additions & 16 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
</PropertyGroup>
<PropertyGroup>
<!-- Aspire -->
<AspireAppHostSdkVersion>13.0.2</AspireAppHostSdkVersion>
<AspireAppHostSdkVersion>13.1.0</AspireAppHostSdkVersion>
</PropertyGroup>
<ItemGroup>
<!-- Aspire.* -->
<PackageVersion Include="Anthropic" Version="12.3.0" />
<PackageVersion Include="Anthropic.Foundry" Version="0.4.1" />
<PackageVersion Include="Aspire.Hosting" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
<PackageVersion Include="Aspire.Azure.AI.Inference" Version="13.1.0-preview.1.25616.3" />
<PackageVersion Include="Aspire.Hosting.Azure.AIFoundry" Version="13.1.0-preview.1.25616.3" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.CognitiveServices" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
Expand Down Expand Up @@ -48,12 +51,12 @@
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
<PackageVersion Include="OpenTelemetry" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
Expand All @@ -70,18 +73,18 @@
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Safety" Version="10.3.0-preview.1.26109.11" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
<!-- Vector Stores -->
Expand Down
11 changes: 11 additions & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
<BuildType Name="Publish" />
<BuildType Name="Release" />
</Configurations>
<Folder Name="/aspire-integration/" />
<Folder Name="/aspire-integration/Aspire.Hosting.AgentFramework.DevUI/">
<Project Path="aspire-integration/Aspire.Hosting.AgentFramework.DevUI/Aspire.Hosting.AgentFramework.DevUI.csproj" />
</Folder>
<Folder Name="/Samples/">
<File Path="samples/AGENTS.md" />
<File Path="samples/README.md" />
Expand Down Expand Up @@ -37,6 +41,12 @@
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIChatCompletion/Agent_With_OpenAIChatCompletion.csproj" />
<Project Path="samples/02-agents/AgentProviders/Agent_With_OpenAIResponses/Agent_With_OpenAIResponses.csproj" />
</Folder>
<Folder Name="/Samples/DevUIIntegration/">
<Project Path="samples/DevUIIntegration/DevUIIntegration.AppHost/DevUIIntegration.AppHost.csproj" />
<Project Path="samples/DevUIIntegration/DevUIIntegration.ServiceDefaults/DevUIIntegration.ServiceDefaults.csproj" />
<Project Path="samples/DevUIIntegration/EditorAgent/EditorAgent.csproj" />
<Project Path="samples/DevUIIntegration/WriterAgent/WriterAgent.csproj" />
</Folder>
<Folder Name="/Samples/02-agents/Agents/">
<File Path="samples/02-agents/Agents/README.md" />
<Project Path="samples/02-agents/Agents/Agent_Step01_UsingFunctionToolsWithApprovals/Agent_Step01_UsingFunctionToolsWithApprovals.csproj" />
Expand Down Expand Up @@ -480,6 +490,7 @@
<Project Path="tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj" />
</Folder>
<Folder Name="/Tests/UnitTests/">
<Project Path="tests/Aspire.Hosting.AgentFramework.DevUI.UnitTests/Aspire.Hosting.AgentFramework.DevUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.

namespace Aspire.Hosting.AgentFramework;

/// <summary>
/// Describes an AI agent exposed by an agent service backend, used for entity discovery in DevUI.
/// </summary>
/// <remarks>
/// <para>
/// When added via <see cref="AgentFrameworkBuilderExtensions.WithAgentService{TSource}"/>,
/// agent metadata is declared at the AppHost level so that the DevUI aggregator can build the
/// entity listing without querying each backend's <c>/v1/entities</c> endpoint.
/// </para>
/// <para>
/// Agent services only need to expose the standard OpenAI Responses and Conversations API endpoints
/// (<c>MapOpenAIResponses</c> and <c>MapOpenAIConversations</c>), not a custom discovery endpoint.
/// </para>
/// </remarks>
/// <param name="Id">The unique identifier for the agent, typically matching the name passed to <c>AddAIAgent</c>.</param>
/// <param name="Description">A short description of the agent's capabilities.</param>
public record AgentEntityInfo(string Id, string? Description = null)
{
/// <summary>
/// Gets the display name for the agent. Defaults to <see cref="Id"/> if not specified.
/// </summary>
public string Name { get; init; } = Id;

/// <summary>
/// Gets the entity type. Defaults to <c>"agent"</c>.
/// </summary>
public string Type { get; init; } = "agent";

/// <summary>
/// Gets the framework identifier. Defaults to <c>"agent_framework"</c>.
/// </summary>
public string Framework { get; init; } = "agent_framework";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Aspire.Hosting.AgentFramework;
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for adding Agent Framework DevUI resources to the application model.
/// </summary>
public static class AgentFrameworkBuilderExtensions
{
/// <summary>
/// Adds a DevUI resource for testing AI agents in a distributed application.
/// </summary>
/// <remarks>
/// <para>
/// DevUI is a web-based interface for testing and debugging AI agents using the OpenAI Responses protocol.
/// When configured with <see cref="WithAgentService{TSource}"/>, it aggregates agents from multiple backend services
/// and provides a unified testing interface.
/// </para>
/// <para>
/// The aggregator runs as an in-process reverse proxy within the AppHost, requiring no external container image.
/// It proxies the DevUI frontend from the first configured backend and aggregates entity listings from all backends.
/// </para>
/// <para>
/// This resource is excluded from the deployment manifest as it is intended for development use only.
/// </para>
/// </remarks>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name to give the resource.</param>
/// <param name="port">The host port for the DevUI web interface. If not specified, a random port will be assigned.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <example>
/// <code>
/// var devui = builder.AddDevUI("devui")
/// .WithAgentService(dotnetAgent)
/// .WithAgentService(pythonAgent);
/// </code>
/// </example>
public static IResourceBuilder<DevUIResource> AddDevUI(
this IDistributedApplicationBuilder builder,
string name,
int? port = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);

var resource = new DevUIResource(name, port);

var resourceBuilder = builder.AddResource(resource)
.ExcludeFromManifest(); // DevUI is a dev-only tool

// Initialize the in-process aggregator when the resource is initialized by the orchestrator
builder.Eventing.Subscribe<InitializeResourceEvent>(resource, async (e, ct) =>
{
var logger = e.Logger;
var aggregator = new DevUIAggregatorHostedService(resource, e.Services.GetRequiredService<ILoggerFactory>().CreateLogger<DevUIAggregatorHostedService>());

try
{
// Wait for dependencies (e.g. agent service backends) before starting.
// Custom resources must manually publish BeforeResourceStartedEvent to trigger
// the orchestrator's WaitFor mechanism.
await e.Eventing.PublishAsync(new BeforeResourceStartedEvent(resource, e.Services), ct).ConfigureAwait(false);

await e.Notifications.PublishUpdateAsync(resource, snapshot => snapshot with
{
State = KnownResourceStates.Starting
}).ConfigureAwait(false);

await aggregator.StartAsync(ct).ConfigureAwait(false);

// Allocate the endpoint so the URL appears in the Aspire dashboard
var endpointAnnotation = resource.Annotations
.OfType<EndpointAnnotation>()
.First(ea => ea.Name == DevUIResource.PrimaryEndpointName);

endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(
endpointAnnotation, "localhost", aggregator.AllocatedPort);

var devuiUrl = $"http://localhost:{aggregator.AllocatedPort}/devui/";

await e.Notifications.PublishUpdateAsync(resource, snapshot => snapshot with
{
State = KnownResourceStates.Running,
Urls = [new UrlSnapshot("DevUI", devuiUrl, IsInternal: false)]
}).ConfigureAwait(false);

// Shut down the aggregator when the app stops
var lifetime = e.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStopping.Register(() =>
{
e.Notifications.PublishUpdateAsync(resource, snapshot => snapshot with
{
State = KnownResourceStates.Finished
}).GetAwaiter().GetResult();

aggregator.StopAsync(CancellationToken.None).GetAwaiter().GetResult();
aggregator.DisposeAsync().AsTask().GetAwaiter().GetResult();
});
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to start DevUI aggregator");

await e.Notifications.PublishUpdateAsync(resource, snapshot => snapshot with
{
State = KnownResourceStates.FailedToStart
}).ConfigureAwait(false);
}
});

return resourceBuilder;
}

/// <summary>
/// Configures DevUI to connect to an agent service backend.
/// </summary>
/// <remarks>
/// <para>
/// Each agent service should expose the OpenAI Responses and Conversations API endpoints
/// (via <c>MapOpenAIResponses</c> and <c>MapOpenAIConversations</c>).
/// </para>
/// <para>
/// When <paramref name="agents"/> is provided, the aggregator builds the entity listing from
/// these declarations without querying the backend. When not provided, a single agent named
/// after the service resource is assumed. Agent services don't need a <c>/v1/entities</c> endpoint.
/// </para>
/// </remarks>
/// <typeparam name="TSource">The type of the agent service resource.</typeparam>
/// <param name="builder">The DevUI resource builder.</param>
/// <param name="agentService">The agent service resource to connect to.</param>
/// <param name="agents">
/// Optional list of agents declared by this backend. When provided, the aggregator uses these
/// declarations directly. When not provided, defaults to a single agent named after the
/// <paramref name="agentService"/> resource. The backend doesn't need to expose a
/// <c>/v1/entities</c> endpoint in either case.
/// </param>
/// <param name="entityIdPrefix">
/// An optional prefix to add to entity IDs from this backend.
/// If not specified, the resource name will be used as the prefix.
/// </param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <example>
/// <code>
/// var writerAgent = builder.AddProject&lt;Projects.WriterAgent&gt;("writer-agent");
/// var editorAgent = builder.AddProject&lt;Projects.EditorAgent&gt;("editor-agent");
///
/// builder.AddDevUI("devui")
/// .WithAgentService(writerAgent, agents: [new("writer", "Writes short stories")])
/// .WithAgentService(editorAgent, agents: [new("editor", "Edits and formats stories")])
/// .WaitFor(writerAgent)
/// .WaitFor(editorAgent);
/// </code>
/// </example>
public static IResourceBuilder<DevUIResource> WithAgentService<TSource>(
this IResourceBuilder<DevUIResource> builder,
IResourceBuilder<TSource> agentService,
IReadOnlyList<AgentEntityInfo>? agents = null,
string? entityIdPrefix = null)
where TSource : IResourceWithEndpoints
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(agentService);

// Default to a single agent named after the service resource
agents ??= [new AgentEntityInfo(agentService.Resource.Name)];

builder.WithAnnotation(new AgentServiceAnnotation(agentService.Resource, entityIdPrefix, agents));
builder.WithRelationship(agentService.Resource, "agent-backend");

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using Aspire.Hosting.AgentFramework;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// An annotation that tracks an agent service backend referenced by a DevUI resource.
/// </summary>
/// <remarks>
/// This annotation is used to configure DevUI to aggregate entities from multiple
/// agent service backends. Each annotation represents one backend that DevUI should
/// connect to for entity discovery and request routing.
/// </remarks>
public class AgentServiceAnnotation : IResourceAnnotation
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentServiceAnnotation"/> class.
/// </summary>
/// <param name="agentService">The agent service resource.</param>
/// <param name="entityIdPrefix">
/// An optional prefix to add to entity IDs from this backend to avoid conflicts.
/// If not specified, the resource name will be used as the prefix.
/// </param>
/// <param name="agents">
/// Optional list of agents declared by this backend. When provided, the aggregator builds the entity
/// listing directly from these declarations instead of querying the backend's <c>/v1/entities</c> endpoint.
/// </param>
public AgentServiceAnnotation(IResource agentService, string? entityIdPrefix = null, IReadOnlyList<AgentEntityInfo>? agents = null)
{
ArgumentNullException.ThrowIfNull(agentService);

this.AgentService = agentService;
this.EntityIdPrefix = entityIdPrefix;
this.Agents = agents ?? [];
}

/// <summary>
/// Gets the agent service resource that exposes AI agents.
/// </summary>
public IResource AgentService { get; }

/// <summary>
/// Gets the prefix to use for entity IDs from this backend.
/// </summary>
/// <remarks>
/// When <c>null</c>, the resource name will be used as the prefix.
/// Entity IDs will be formatted as "{prefix}/{entityId}" to ensure uniqueness
/// across multiple agent backends.
/// </remarks>
public string? EntityIdPrefix { get; }

/// <summary>
/// Gets the list of agents declared by this backend.
/// </summary>
/// <remarks>
/// When non-empty, the DevUI aggregator uses these declarations to build the entity listing
/// without querying the backend. When empty, the aggregator falls back to calling
/// <c>GET /v1/entities</c> on the backend for discovery.
/// </remarks>
public IReadOnlyList<AgentEntityInfo> Agents { get; }
}
Loading
Loading