Skip to content
Open
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 @@ -26,6 +26,7 @@
.WithReference(project)
.WithReference(deployment)
.WaitFor(deployment)
.AsHostedAgent()
.WithComputeEnvironment(project, (opts) =>
{
opts.Description = "Foundry Agent Basic Example";
Expand Down
2 changes: 2 additions & 0 deletions playground/FoundryAgents/FoundryAgents.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@
builder.AddPythonApp("weather-hosted-agent", "../app", "main.py")
.WithUv()
.WithReference(project).WithReference(chat).WaitFor(chat)
.AsHostedAgent()
.WithComputeEnvironment(project);

builder.AddProject<Projects.DotNetHostedAgent>("proj-dotnet-hosted-agent")
.WithHttpEndpoint(targetPort: 9000)
.WithReference(project).WithReference(chat).WaitFor(chat)
.AsHostedAgent()
.WithComputeEnvironment(project);

// --- Prompt Agents ---
Expand Down
277 changes: 146 additions & 131 deletions src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ public static class HostedAgentResourceBuilderExtensions
{

/// <summary>
/// Configures the resource to run as a hosted agent in Microsoft Foundry.
///
/// If a project resource is not provided, the method will attempt to find an existing
/// Microsoft Foundry project resource in the application model. If none exists,
/// a new project resource (and its parent account resource) will be created automatically.
/// Configures the resource to be deployed as a hosted agent in Microsoft Foundry.
/// </summary>
/// <typeparam name="T">The type of resource being configured.</typeparam>
/// <param name="builder">The resource builder for the compute resource.</param>
/// <param name="configure">A callback to configure the hosted agent deployment.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <remarks>
/// In run mode, this configures the resource with hosted agent endpoints, health checks,
/// and OpenTelemetry settings. In publish mode, the resource is deployed as a hosted agent
/// in Microsoft Foundry.
/// This method applies in publish mode. Use <see cref="AsHostedAgent{T}(IResourceBuilder{T})"/>
/// to configure the resource with local run-mode hosted agent endpoints, dashboard commands,
/// and OpenTelemetry settings.
/// </remarks>
[AspireExportIgnore(Reason = "Subset of the full WithComputeEnvironment overload which is exported.")]
public static IResourceBuilder<T> WithComputeEnvironment<T>(
Expand All @@ -37,36 +37,158 @@ public static IResourceBuilder<T> WithComputeEnvironment<T>(
}

/// <summary>
/// Configures the resource to run as a hosted agent in Microsoft Foundry.
///
/// If a project resource is not provided, the method will attempt to find an existing
/// Microsoft Foundry project resource in the application model. If none exists,
/// a new project resource (and its parent account resource) will be created automatically.
/// Configures the resource to be deployed as a hosted agent in Microsoft Foundry.
/// </summary>
/// <typeparam name="T">The type of resource being configured.</typeparam>
/// <param name="builder">The resource builder for the compute resource.</param>
/// <param name="project">The Microsoft Foundry project resource to deploy the hosted agent to.</param>
/// <param name="configure">A callback to configure the hosted agent deployment.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <remarks>
/// In run mode, this configures the resource with hosted agent endpoints, health checks,
/// and OpenTelemetry settings. In publish mode, the resource is deployed as a hosted agent
/// in Microsoft Foundry.
/// <para>
/// If <paramref name="project"/> is not provided, this method attempts to find an existing Microsoft Foundry
/// project resource in the application model. If none exists, a new project resource and parent account resource
/// are created automatically.
/// </para>
/// <para>
/// This method applies in publish mode. Use <see cref="AsHostedAgent{T}(IResourceBuilder{T})"/>
/// to configure the resource with local run-mode hosted agent endpoints, dashboard commands,
/// and OpenTelemetry settings.
/// </para>
/// </remarks>
[AspireExport("withComputeEnvironmentExecutable", MethodName = "withComputeEnvironment")]
public static IResourceBuilder<T> WithComputeEnvironment<T>(
this IResourceBuilder<T> builder, IResourceBuilder<AzureCognitiveServicesProjectResource>? project = null, Action<HostedAgentConfiguration>? configure = null)
where T : IResourceWithEndpoints, IResourceWithEnvironment, IComputeResource
{
/*
* 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);
if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
{
/*
* 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;

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
.AddFoundry($"{resource.Name}-proj-foundry")
.AddProject($"{resource.Name}-proj");
projResource = project.Resource;
}
else
{
project = builder.ApplicationBuilder.CreateResourceBuilder(projResource);
}
}

var resource = builder.Resource;
ResourceBuilderExtensions.WithComputeEnvironment(builder, project!);

// 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 for ExecutableResources. Usually this is swapped in at publish time for ExecutableResources.
IResource 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.
// ExecutableResource needs PublishAsDockerFile()
// to convert them into container resources at this stage.
if (resource is ExecutableResource)
{
builder.ApplicationBuilder.CreateResourceBuilder((ExecutableResource)(object)resource).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.");
}
}
else if (resource is not ProjectResource)
{
throw new InvalidOperationException($"Unable to create hosted agent for resource '{resource.Name}' because it is not a container, executable, or project resource.");
}
else
{
target = 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>
/// Configures the resource to run locally as a Microsoft Foundry hosted agent.
/// </summary>
/// <ats-summary>Configures the resource to run locally as a Microsoft Foundry hosted agent.</ats-summary>
/// <typeparam name="T">The type of resource being configured.</typeparam>
/// <param name="builder">The resource builder for the compute resource.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <remarks>
/// This method applies in run mode. It configures the resource with the hosted agent responses endpoint,
/// a dashboard command for sending messages to the agent, and OpenTelemetry environment variables expected
/// by the Microsoft Foundry agent server SDK.
/// </remarks>
/// <example>
/// <code lang="csharp">
/// var agent = builder.AddProject&lt;Projects.AgentService&gt;("agent")
/// .AsHostedAgent();
/// </code>
/// </example>
/// <ats-returns>The resource builder.</ats-returns>
[AspireExport]
public static IResourceBuilder<T> AsHostedAgent<T>(this IResourceBuilder<T> builder)
where T : IResourceWithEndpoints, IResourceWithEnvironment, IComputeResource
{
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
// Preserve any target port already configured on an existing "http" endpoint;
// fall back to the default MAF agent port (8088) when none is set.
var existingHttpEndpoint = resource.Annotations.OfType<EndpointAnnotation>().FirstOrDefault(e => e.Name == "http");
var existingHttpEndpoint = builder.Resource.Annotations.OfType<EndpointAnnotation>().FirstOrDefault(e => e.Name == "http");
var targetPort = existingHttpEndpoint?.TargetPort ?? 8088;

builder
Expand All @@ -83,28 +205,7 @@ public static IResourceBuilder<T> WithComputeEnvironment<T>(
{
Path = "/responses"
}.ToString();
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",
Expand Down Expand Up @@ -198,93 +299,7 @@ await interactionService.PromptMessageBoxAsync(
// 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
.AddFoundry($"{resource.Name}-proj-foundry")
.AddProject($"{resource.Name}-proj");
projResource = project.Resource;
}
else
{
project = builder.ApplicationBuilder.CreateResourceBuilder(projResource);
}
}

ResourceBuilderExtensions.WithComputeEnvironment(builder, project!);

// 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 for ExecutableResources. Usually this is swapped in at publish time for ExecutableResources.
IResource 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.
// ExecutableResource needs PublishAsDockerFile()
// to convert them into container resources at this stage.
if (resource is ExecutableResource)
{
builder.ApplicationBuilder.CreateResourceBuilder((ExecutableResource)(object)resource).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.");
}
}
else if (resource is not ProjectResource)
{
throw new InvalidOperationException($"Unable to create hosted agent for resource '{resource.Name}' because it is not a container, executable, or project resource.");
}
else
{
target = 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ Aspire.Hosting.Foundry/HostedAgentConfiguration.metadata(context: Aspire.Hosting
Aspire.Hosting.Foundry/HostedAgentConfiguration.setCpu(context: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.HostedAgentConfiguration, value: number) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.HostedAgentConfiguration
Aspire.Hosting.Foundry/HostedAgentConfiguration.setDescription(context: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.HostedAgentConfiguration, value: string) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.HostedAgentConfiguration
Aspire.Hosting.Foundry/HostedAgentConfiguration.setMemory(context: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.HostedAgentConfiguration, value: number) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.HostedAgentConfiguration
Aspire.Hosting.Foundry/asHostedAgent() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints
Aspire.Hosting.Foundry/withComputeEnvironmentExecutable(project?: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource, configure?: callback) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints
Aspire.Hosting.Foundry/runAsFoundryLocal() -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.FoundryResource
Aspire.Hosting.Foundry/withAppInsights(appInsights: Aspire.Hosting.Azure.ApplicationInsights/Aspire.Hosting.Azure.AzureApplicationInsightsResource) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource
Aspire.Hosting.Foundry/withBingReference(bingReference: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.BingGroundingConnectionResource|string|Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.BingGroundingToolResource
Expand Down
4 changes: 4 additions & 0 deletions src/Aspire.Hosting.Foundry/api/Aspire.Hosting.Foundry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ public static ApplicationModel.IResourceBuilder<T> WithRoleAssignments<T>(this A

public static partial class HostedAgentResourceBuilderExtensions
{
[AspireExport]
public static ApplicationModel.IResourceBuilder<T> AsHostedAgent<T>(this ApplicationModel.IResourceBuilder<T> builder)
where T : ApplicationModel.IResourceWithEndpoints, ApplicationModel.IResourceWithEnvironment, ApplicationModel.IComputeResource { throw null; }

[AspireExport("withComputeEnvironmentExecutable", MethodName = "withComputeEnvironment")]
public static ApplicationModel.IResourceBuilder<T> WithComputeEnvironment<T>(this ApplicationModel.IResourceBuilder<T> builder, ApplicationModel.IResourceBuilder<Foundry.AzureCognitiveServicesProjectResource>? project = null, System.Action<Foundry.HostedAgentConfiguration>? configure = null)
where T : ApplicationModel.IResourceWithEndpoints, ApplicationModel.IResourceWithEnvironment, ApplicationModel.IComputeResource { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ static string GetRequiredConnectionValue(DbConnectionStringBuilder connectionBui

builder.AddProject<Projects.DotNetHostedAgent>("dotnet-hosted-agent")
.WithReference(chat).WaitFor(chat)
.AsHostedAgent()
.WithComputeEnvironment(foundryProject);

builder.Build().Run();
Expand Down
Loading
Loading