chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
// 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 serves the DevUI frontend from embedded resources in Microsoft.Agents.AI.DevUI when available, and
|
||||
/// falls back to proxying from the first configured backend. It 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 aggregator.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
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<Projects.WriterAgent>("writer-agent");
|
||||
/// var editorAgent = builder.AddProject<Projects.EditorAgent>("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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user