|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Foundry;
using Microsoft.Extensions.DependencyInjection;
namespace Aspire.Hosting;
/// <summary>
/// Extension methods for adding hosted agent applications to the distributed application model.
/// </summary>
public static class HostedAgentResourceBuilderExtensions
{
/// <summary>
/// In both run and publish modes, build, deploy, and run the containerized agent as a hosted agent in Microsoft Foundry.
/// </summary>
/// <remarks>This overload is not available in polyglot app hosts. Use other Foundry project APIs to configure hosted agents from .NET.</remarks>
[AspireExportIgnore(Reason = "HostedAgentConfiguration callback parameters are not ATS-compatible.")]
public static IResourceBuilder<T> AsHostedAgent<T>(
this IResourceBuilder<T> builder, Action<HostedAgentConfiguration>? configure = null)
where T : ExecutableResource
{
return builder.AsHostedAgent(project: null, configure: configure);
}
/// <summary>
/// In both run and publish modes, build, deploy, and run the containerized agent as a hosted agent in Microsoft Foundry.
/// </summary>
/// <remarks>This overload is not available in polyglot app hosts. Use other Foundry project APIs to configure hosted agents from .NET.</remarks>
[AspireExportIgnore(Reason = "HostedAgentConfiguration callback parameters are not ATS-compatible.")]
public static IResourceBuilder<T> AsHostedAgent<T>(
this IResourceBuilder<T> builder, IResourceBuilder<AzureCognitiveServicesProjectResource>? project = null, Action<HostedAgentConfiguration>? configure = null)
where T : ExecutableResource
{
return builder.RunAsHostedAgent(project: project, configure: configure).PublishAsHostedAgent(project: project, configure: configure);
}
/// <summary>
/// In run mode, build, deploy, and run the containerized agent as a hosted agent in Microsoft Foundry.
/// </summary>
/// <remarks>This overload is not available in polyglot app hosts. Use other Foundry project APIs to configure hosted agents from .NET.</remarks>
[AspireExportIgnore(Reason = "HostedAgentConfiguration callback parameters are not ATS-compatible.")]
public static IResourceBuilder<T> RunAsHostedAgent<T>(
this IResourceBuilder<T> builder, Action<HostedAgentConfiguration> configure)
where T : ExecutableResource
{
return builder.RunAsHostedAgent(project: null, configure: configure);
}
/// <summary>
/// In run mode, build, deploy, and run the containerized agent as a hosted agent in Microsoft Foundry.
/// </summary>
/// <remarks>This overload is not available in polyglot app hosts. Use other Foundry project APIs to configure hosted agents from .NET.</remarks>
[AspireExportIgnore(Reason = "HostedAgentConfiguration callback parameters are not ATS-compatible.")]
public static IResourceBuilder<T> RunAsHostedAgent<T>(
this IResourceBuilder<T> builder, IResourceBuilder<AzureCognitiveServicesProjectResource>? project = null, Action<HostedAgentConfiguration>? configure = null)
where T : ExecutableResource
{
// TODO: Implement this. This will require
// 1. Ensuring that ACR is provisioned
// 2. Building and pushing the container image
// 3. Creating an agent version and returning the name/version of the agent for later use.
throw new NotImplementedException("RunAsHostedAgent is not yet implemented.");
}
/// <summary>
/// Publish the containerized agent as a hosted agent in Microsoft Foundry.
///
/// If a project resource is not provided, the method will attempt to find an existing
/// Azure Cognitive Services Project resource in the application model. If none exists,
/// a new project resource (and its parent account resource) will be created automatically.
/// </summary>
/// <remarks>This overload is not available in polyglot app hosts. Use other Foundry project APIs to configure hosted agents from .NET.</remarks>
[AspireExportIgnore(Reason = "HostedAgentConfiguration callback parameters are not ATS-compatible.")]
public static IResourceBuilder<T> PublishAsHostedAgent<T>(
this IResourceBuilder<T> builder, Action<HostedAgentConfiguration> configure)
where T : ExecutableResource
{
return PublishAsHostedAgent(builder, project: null, configure: configure);
}
/// <summary>
/// Publish the containerized agent as a hosted agent in Microsoft Foundry.
///
/// If a project resource is not provided, the method will attempt to find an existing
/// Azure Cognitive Services Project resource in the application model. If none exists,
/// a new project resource (and its parent account resource) will be created automatically.
/// </summary>
/// <remarks>This overload is not available in polyglot app hosts. Use other Foundry project APIs to configure hosted agents from .NET.</remarks>
[AspireExportIgnore(Reason = "HostedAgentConfiguration callback parameters are not ATS-compatible.")]
public static IResourceBuilder<T> PublishAsHostedAgent<T>(
this IResourceBuilder<T> builder, IResourceBuilder<AzureCognitiveServicesProjectResource>? project = null, Action<HostedAgentConfiguration>? configure = null)
where T : ExecutableResource
{
/*
* Much of the logic here is similar to ExecutableResourceBuilderExtensions.PublishAsDockerFile().
*
* That is, in Publish mode, we swap the original resource with a hosted agent resource.
*/
ArgumentNullException.ThrowIfNull(builder);
var resource = builder.Resource;
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
builder
.WithHttpEndpoint(name: "http", env: "DEFAULT_AD_PORT", port: 8088, targetPort: 8088, isProxied: false)
.WithUrls((ctx) =>
{
var http = ctx.Urls.FirstOrDefault(u => u.Endpoint?.EndpointName == "http" || u.Endpoint?.EndpointName == "https");
if (http is null)
{
return;
}
ctx.Urls.Add(new()
{
DisplayText = "Responses endpoint",
Url = new UriBuilder(http.Url)
{
Path = "/responses"
}.ToString(),
Endpoint = http.Endpoint,
});
ctx.Urls.Add(new()
{
DisplayText = "Liveness probe",
Url = new UriBuilder(http.Url)
{
Path = "/liveness"
}.ToString(),
Endpoint = http.Endpoint,
DisplayLocation = UrlDisplayLocation.DetailsOnly
});
ctx.Urls.Add(new()
{
DisplayText = "Readiness probe",
Url = new UriBuilder(http.Url)
{
Path = "/readiness"
}.ToString(),
Endpoint = http.Endpoint,
DisplayLocation = UrlDisplayLocation.DetailsOnly
});
})
.WithHttpHealthCheck("/liveness")
.WithHttpCommand(
path: "/responses",
displayName: "Send Message",
endpointName: "http",
commandOptions: new()
{
Method = HttpMethod.Post,
IconName = "Agents",
IconVariant = IconVariant.Regular,
IsHighlighted = true,
PrepareRequest = async ctx =>
{
var interactionService = ctx.ServiceProvider.GetRequiredService<IInteractionService>();
var result = await interactionService.PromptInputAsync(
title: "Responses API",
message: "Enter a message to send to the agent.",
inputLabel: "Message",
placeHolder: "I would like to know the weather today.",
cancellationToken: ctx.CancellationToken
).ConfigureAwait(true);
if (result.Canceled || string.IsNullOrWhiteSpace(result.Data.Value))
{
ctx.HttpClient.CancelPendingRequests();
throw new OperationCanceledException("User canceled the input prompt.");
}
var request = ctx.Request;
var input = result.Data.Value;
request.Content = new StringContent(new JsonObject() { ["input"] = input }.ToString(), System.Text.Encoding.UTF8, "application/json");
},
GetCommandResult = async ctx =>
{
ctx.CancellationToken.ThrowIfCancellationRequested();
try
{
var response = await ctx.Response
.EnsureSuccessStatusCode()
.Content
.ReadFromJsonAsync<JsonObject>(cancellationToken: ctx.CancellationToken)
.ConfigureAwait(true);
var formattedResponse = $"```\n{JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true })}\n```";
var interactionService = ctx.ServiceProvider.GetRequiredService<IInteractionService>();
await interactionService.PromptMessageBoxAsync(
title: "Agent Response",
message: formattedResponse,
options: new()
{
Intent = MessageIntent.Success,
EnableMessageMarkdown = true,
PrimaryButtonText = "Thanks!"
},
cancellationToken: ctx.CancellationToken
).ConfigureAwait(true);
return new() { Success = true };
}
catch (Exception ex)
{
var interactionService = ctx.ServiceProvider.GetRequiredService<IInteractionService>();
await interactionService.PromptMessageBoxAsync(
title: "Error",
message: $"An error occurred while processing the agent's response: {ex.Message}",
options: new()
{
Intent = MessageIntent.Error,
PrimaryButtonText = "OK"
},
cancellationToken: ctx.CancellationToken
).ConfigureAwait(true);
Console.Error.Write($"Error processing agent response: {ex}");
return new() { Success = false };
}
},
}
)
.WithOtlpExporter()
.WithEnvironment((ctx) =>
{
ctx.EnvironmentVariables.Add("OTEL_INSTRUMENTATION_OPENAI_AGENTS_ENABLED", "true");
ctx.EnvironmentVariables.Add("OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_CONTENT", "true");
ctx.EnvironmentVariables.Add("OTEL_INSTRUMENTATION_OPENAI_AGENTS_CAPTURE_METRICS", "true");
ctx.EnvironmentVariables.Add("OTEL_GENAI_CAPTURE_MESSAGES", "true");
ctx.EnvironmentVariables.Add("OTEL_GENAI_CAPTURE_SYSTEM_INSTRUCTIONS", "true");
ctx.EnvironmentVariables.Add("OTEL_GENAI_CAPTURE_TOOL_DEFINITIONS", "true");
ctx.EnvironmentVariables.Add("OTEL_GENAI_EMIT_OPERATION_DETAILS", "true");
ctx.EnvironmentVariables.Add("OTEL_GENAI_AGENT_NAME", ctx.Resource.Name);
ctx.EnvironmentVariables.Add("OTEL_GENAI_AGENT_ID", ctx.Resource.Name);
var endpointVar = ctx.EnvironmentVariables.FirstOrDefault((item) => item.Key == "OTEL_EXPORTER_OTLP_ENDPOINT");
if (endpointVar.Equals(default(KeyValuePair<string, string>)))
{
return;
}
// The Microsoft Foundry agentserver SDK expects the exporter to be at OTEL_EXPORTER_ENDPOINT instead.
ctx.EnvironmentVariables["OTEL_EXPORTER_ENDPOINT"] = endpointVar.Value;
});
return builder;
}
AzureCognitiveServicesProjectResource? projResource;
if (project is not null)
{
projResource = project.Resource;
}
else
{
projResource = builder.ApplicationBuilder.Resources.OfType<AzureCognitiveServicesProjectResource>().FirstOrDefault();
if (projResource is null)
{
project = builder.ApplicationBuilder.AddFoundryProject($"{resource.Name}-proj");
projResource = project.Resource;
}
else
{
project = builder.ApplicationBuilder.CreateResourceBuilder(projResource);
}
}
// Hosted Agent resource name
var agentName = $"{resource.Name}-ha";
if (builder.ApplicationBuilder.TryCreateResourceBuilder<AzureHostedAgentResource>(agentName, out var rb))
{
// We already have a hosted agent for this resource
if (configure is not null)
{
rb.Resource.Configure = configure;
}
return builder;
}
// Get the corresponding ContainerResource. Usually this is swapped in at publish time for ExecutableResources.
ContainerResource target;
if (resource is ContainerResource containerResource)
{
target = containerResource;
}
else if (builder.ApplicationBuilder.TryCreateResourceBuilder<ContainerResource>(resource.Name, out var crb))
{
target = crb.Resource;
}
else
{
// Ensure we have a container resource to deploy
builder.PublishAsDockerFile();
if (builder.ApplicationBuilder.TryCreateResourceBuilder(resource.Name, out crb))
{
target = crb.Resource;
}
else
{
throw new InvalidOperationException($"Unable to create hosted agent for resource '{resource.Name}' because it could not be converted to a container resource.");
}
}
// Create a separate agent resource to host the deployment
var agent = new AzureHostedAgentResource(agentName, target, configure);
// Ensure image gets pushed properly
target.Annotations.Add(new DeploymentTargetAnnotation(agent)
{
ComputeEnvironment = projResource,
ContainerRegistry = projResource.ContainerRegistry
});
builder.ApplicationBuilder.AddResource(agent)
.WithReferenceRelationship(target)
.WithReference(project);
return builder;
}
/// <summary>
/// Publish a simple prompt agent in Microsoft Foundry.
///
/// If a project resource is not provided, the method will attempt to find an existing
/// Azure Cognitive Services Project resource in the application model.
/// </summary>
[AspireExport("addAndPublishPromptAgent", Description = "Adds and publishes a prompt agent to a Microsoft Foundry project.")]
public static IResourceBuilder<AzurePromptAgentResource> AddAndPublishPromptAgent(
this IResourceBuilder<AzureCognitiveServicesProjectResource> project, IResourceBuilder<FoundryDeploymentResource> model, [ResourceName] string name, string? instructions)
{
ArgumentNullException.ThrowIfNull(project);
ArgumentNullException.ThrowIfNull(model);
var agent = new AzurePromptAgentResource(name, model.Resource.DeploymentName, instructions);
return project.ApplicationBuilder.AddResource(agent)
.WithReferenceRelationship(project)
.WithArgs([
// TODO: actually execute the prompt agent locally
"-c",
"--project", project.Resource.Endpoint,
"--model", model.Resource.DeploymentName,
"--instructions", instructions ?? string.Empty,
]);
}
}
|