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 @@ -23,10 +23,9 @@
.WithHttpEndpoint()
.WithExternalHttpEndpoints()
.WithHttpHealthCheck("/health")
.WithReference(project)
.WithReference(deployment)
.WaitFor(deployment)
.WithComputeEnvironment(project, (opts) =>
.AsHostedAgent(project, (opts) =>
{
opts.Description = "Foundry Agent Basic Example";
opts.Metadata["managed-by"] = "aspire-foundry";
Expand Down
8 changes: 4 additions & 4 deletions playground/FoundryAgents/FoundryAgents.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@

builder.AddPythonApp("weather-hosted-agent", "../app", "main.py")
.WithUv()
.WithReference(project).WithReference(chat).WaitFor(chat)
.WithComputeEnvironment(project);
.WithReference(chat).WaitFor(chat)
.AsHostedAgent(project);

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

// --- Prompt Agents ---

Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Hosting.Foundry/FoundryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static IResourceBuilder<FoundryResource> AddFoundry(this IDistributedAppl

var resource = new FoundryResource(name, ConfigureInfrastructure);
return builder.AddResource(resource)
.WithIconName("AgentsAdd")
.WithDefaultRoleAssignments(CognitiveServicesBuiltInRole.GetBuiltInRoleName,
CognitiveServicesBuiltInRole.CognitiveServicesUser, CognitiveServicesBuiltInRole.CognitiveServicesOpenAIUser);
}
Expand Down Expand Up @@ -77,7 +78,7 @@ public static IResourceBuilder<FoundryDeploymentResource> AddDeployment(this IRe
deploymentBuilder.AsLocalDeployment(deployment);
}

return deploymentBuilder;
return deploymentBuilder.WithIconName("BoxMultiple");
}

/// <summary>
Expand Down
479 changes: 267 additions & 212 deletions src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.Foundry;

// HostedAgentOptions exposes the subset of HostedAgentConfiguration that is meaningful to non-.NET
// app hosts. .NET callers should use the AsHostedAgent overload that takes Action<HostedAgentConfiguration>
// to access the full configuration surface (tools, content filters, container protocol versions, etc.).

/// <summary>
/// Options that control how a compute resource is deployed as a Microsoft Foundry hosted agent.
/// All properties are optional; unset properties fall back to the Foundry hosted agent defaults.
/// </summary>
[AspireDto]
internal sealed class HostedAgentOptions
{
/// <summary>
/// Human-readable description of the hosted agent surfaced in the Microsoft Foundry portal.
/// When not set, the hosted agent default description is used.
/// </summary>
public string? Description { get; set; }

/// <summary>
/// CPU allocation for each hosted agent instance, in vCPU cores. Must be between 0.5 and 3.5
/// in increments of 0.25. When not set, the hosted agent default CPU allocation is used.
/// </summary>
public decimal? Cpu { get; set; }

/// <summary>
/// Memory allocation for each hosted agent instance, in GiB. Must be between 1 and 7 in
/// increments of 0.5 and equal to twice the CPU value. When not set, the hosted agent
/// default memory allocation is used.
/// </summary>
public decimal? Memory { get; set; }

/// <summary>
/// Additional metadata key/value pairs to attach to the hosted agent definition.
/// Entries with the same key as an existing metadata entry overwrite it.
/// </summary>
public IDictionary<string, string> Metadata { get; init; } = new Dictionary<string, string>();

/// <summary>
/// Environment variables to set on the hosted agent container at runtime.
/// Entries with the same key as an existing environment variable overwrite it.
/// </summary>
public IDictionary<string, string> EnvironmentVariables { get; init; } = new Dictionary<string, string>();

internal void ApplyTo(HostedAgentConfiguration configuration)
{
if (Description is not null)
{
configuration.Description = Description;
}

// Cpu and Memory have a coupled invariant on HostedAgentConfiguration (Memory = Cpu * 2 with validation).
// Apply Cpu first so a subsequent Memory assignment can still override the derived value.
if (Cpu is { } cpu)
{
configuration.Cpu = cpu;
}

if (Memory is { } memory)
{
configuration.Memory = memory;
}

foreach (var kvp in Metadata)
{
configuration.Metadata[kvp.Key] = kvp.Value;
}

foreach (var kvp in EnvironmentVariables)
{
configuration.EnvironmentVariables[kvp.Key] = kvp.Value;
}
}
}
85 changes: 49 additions & 36 deletions src/Aspire.Hosting.Foundry/Project/ProjectBuilderExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ public static IResourceBuilder<AzureCognitiveServicesProjectResource> AddProject
builder.ApplicationBuilder.Services.Configure<AzureProvisioningOptions>(o => o.SupportsTargetedRoleAssignments = true);

var project = builder.ApplicationBuilder.AddResource(new AzureCognitiveServicesProjectResource(name, ConfigureInfrastructure, builder.Resource));
project.Resource.DefaultContainerRegistry = CreateDefaultRegistry(builder.ApplicationBuilder, $"{name}-acr");
if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
{
project.Resource.DefaultContainerRegistry = CreateDefaultRegistry(builder.ApplicationBuilder, $"{name}-acr");
}

return project;
}

Expand Down Expand Up @@ -324,6 +328,12 @@ public static IResourceBuilder<FoundryDeploymentResource> AddModelDeployment(
return builder.ApplicationBuilder.CreateResourceBuilder(builder.Resource.Parent).AddDeployment(name, modelName, modelVersion, format);
}

private static bool RequiresContainerRegistryProvisioning(AzureCognitiveServicesProjectResource project)
{
return project.HasAnnotationOfType<RequiresHostedAgentRegistryAnnotation>()
|| project.HasAnnotationOfType<ContainerRegistryReferenceAnnotation>();
}

internal static void ConfigureInfrastructure(AzureResourceInfrastructure infra)
{
var prefix = infra.AspireResource.Name;
Expand Down Expand Up @@ -411,44 +421,47 @@ internal static void ConfigureInfrastructure(AzureResourceInfrastructure infra)
/*
* Container registry for hosted agents
*
* TODO: only provision if we need to create a Hosted Agent
* Only provision registry dependencies when the project will publish a hosted agent
* or when the user has explicitly supplied a registry override.
*/

AzureProvisioningResource? registry = null;
if (aspireResource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var registryReferenceAnnotation) && registryReferenceAnnotation.Registry is AzureProvisioningResource r)
{
registry = r;
}
else if (aspireResource.DefaultContainerRegistry is not null)
if (RequiresContainerRegistryProvisioning(aspireResource))
{
registry = aspireResource.DefaultContainerRegistry;
}
else
{
throw new InvalidOperationException($"No container registry configured for Azure Cognitive Services project resource '{aspireResource.Name}'. A container registry is required to publish and run hosted agents.");
AzureProvisioningResource? registry = null;
if (aspireResource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var registryReferenceAnnotation) && registryReferenceAnnotation.Registry is AzureProvisioningResource r)
{
registry = r;
}
else if (aspireResource.DefaultContainerRegistry is not null)
{
registry = aspireResource.DefaultContainerRegistry;
}
else
{
throw new InvalidOperationException($"No container registry configured for Azure Cognitive Services project resource '{aspireResource.Name}'. A container registry is required to publish hosted agents.");
}

var containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra);
infra.Add(containerRegistry);

// Project needs this to pull hosted agent images during hosted-agent deployment.
var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, RoleManagementPrincipalType.ServicePrincipal, projectPrincipalId);
// There's a bug in the CDK, see https://github.com/Azure/azure-sdk-for-net/issues/47265
pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, project.Id, pullRa.RoleDefinitionId);
infra.Add(pullRa);
infra.Add(containerRegistry);
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_ENDPOINT", typeof(string))
{
Value = containerRegistry.LoginServer
});
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_NAME", typeof(string))
{
Value = containerRegistry.Name
});
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", typeof(string))
{
Value = projectPrincipalId
});
}
var containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra);
// Why do we need this?
infra.Add(containerRegistry);

// Project needs this to pull hosted agent images and run them
var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, RoleManagementPrincipalType.ServicePrincipal, projectPrincipalId);
// There's a bug in the CDK, see https://github.com/Azure/azure-sdk-for-net/issues/47265
pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, project.Id, pullRa.RoleDefinitionId);
infra.Add(pullRa);
infra.Add(containerRegistry);
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_ENDPOINT", typeof(string))
{
Value = containerRegistry.LoginServer
});
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_NAME", typeof(string))
{
Value = containerRegistry.Name
});
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", typeof(string))
{
Value = projectPrincipalId
});

// Implicit dependencies for capability hosts
List<ProvisionableResource> capHostDeps = [];
Expand Down
12 changes: 10 additions & 2 deletions src/Aspire.Hosting.Foundry/Project/ProjectResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ public AzureCognitiveServicesProjectResource([ResourceName] string name, Action<
Description = $"Prepares Microsoft Foundry project {name} for deployment.",
Action = context =>
{
if (this.HasAnnotationOfType<ContainerRegistryReferenceAnnotation>() &&
DefaultContainerRegistry is not null)
if (DefaultContainerRegistry is not null &&
(this.HasAnnotationOfType<ContainerRegistryReferenceAnnotation>() ||
!this.HasAnnotationOfType<RequiresHostedAgentRegistryAnnotation>()))
{
context.Model.Resources.Remove(DefaultContainerRegistry);
DefaultContainerRegistry = null;
Expand Down Expand Up @@ -248,6 +249,13 @@ public bool TryGetAppIdentityResource([NotNullWhen(true)] out IAppIdentityResour
}
}

/// <summary>
/// Marks a Foundry project as needing container registry provisioning for hosted agent deployment.
/// </summary>
internal sealed class RequiresHostedAgentRegistryAnnotation : IResourceAnnotation
{
}

/// <summary>
/// Configuration for a Microsoft Foundry capability host.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public static IResourceBuilder<AzurePromptAgentResource> AddPromptAgent(
var agent = new AzurePromptAgentResource(name, model.Resource.DeploymentName, project.Resource, instructions);

var agentBuilder = project.ApplicationBuilder.AddResource(agent)
.WithIconName("Agents")
.WithReferenceRelationship(project)
.WithReference(project);

Expand Down Expand Up @@ -103,7 +104,7 @@ public static IResourceBuilder<AzurePromptAgentResource> AddPromptAgent(
},
commandOptions: new()
{
IconName = "Agents",
IconName = "ChatSparkle",
IconVariant = IconVariant.Regular,
IsHighlighted = true,
Arguments =
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting.Foundry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ var foundry = builder.AddFoundry("foundry");
var project = foundry.AddProject("my-project");

builder.AddPythonApp("agent", "./app", "main:app")
.WithComputeEnvironment(project);
.AsHostedAgent(project);
```

In run mode, the agent runs locally with health check endpoints and OpenTelemetry instrumentation. In publish mode, the agent is deployed as a hosted agent in Microsoft Foundry.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.FoundryModel # Describes a model t
Format: string # The format or provider of the model (e.g., OpenAI, Microsoft, xAi, Deepseek).
Name: string # The name of the model.
Version: string # The version of the model.
Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.HostedAgentOptions # Options that control how a compute resource is deployed as a Microsoft Foundry hosted agent. All properties are optional; unset properties fall back to the Foundry hosted agent defaults.
Cpu?: number # CPU allocation for each hosted agent instance, in vCPU cores. Must be between 0.5 and 3.5 in increments of 0.25. When not set, the hosted agent default CPU allocation is used.
Description: string # Human-readable description of the hosted agent surfaced in the Microsoft Foundry portal. When not set, the hosted agent default description is used.
EnvironmentVariables?: Aspire.Hosting/Dict<string,string> # Environment variables to set on the hosted agent container at runtime. Entries with the same key as an existing environment variable overwrite it.
Memory?: number # Memory allocation for each hosted agent instance, in GiB. Must be between 1 and 7 in increments of 0.5 and equal to twice the CPU value. When not set, the hosted agent default memory allocation is used.
Metadata?: Aspire.Hosting/Dict<string,string> # Additional metadata key/value pairs to attach to the hosted agent definition. Entries with the same key as an existing metadata entry overwrite it.

# Enum Types
enum:Aspire.Hosting.FoundryRole = CognitiveServicesOpenAIContributor | CognitiveServicesOpenAIUser | CognitiveServicesUser
Expand Down Expand Up @@ -262,6 +268,7 @@ Aspire.Hosting.Foundry/addSearchConnection(search: Aspire.Hosting.Azure.Search/A
Aspire.Hosting.Foundry/addSharePointTool(name: string, projectConnectionIds: string[]) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.SharePointToolResource
Aspire.Hosting.Foundry/addStorageConnection(storage: Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.AzureStorageResource) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectConnectionResource
Aspire.Hosting.Foundry/addWebSearchTool(name: string) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.WebSearchToolResource
Aspire.Hosting.Foundry/asHostedAgentExecutable(project: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource, options?: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.HostedAgentOptions) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints
Aspire.Hosting.Foundry/AzurePromptAgentResource.connectionStringExpression(context: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzurePromptAgentResource) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression
Aspire.Hosting.Foundry/AzurePromptAgentResource.description(context: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzurePromptAgentResource) -> string
Aspire.Hosting.Foundry/AzurePromptAgentResource.instructions(context: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzurePromptAgentResource) -> string
Expand Down Expand Up @@ -297,7 +304,6 @@ Aspire.Hosting.Foundry/runAsFoundryLocal() -> Aspire.Hosting.Foundry/Aspire.Host
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
Aspire.Hosting.Foundry/withCapabilityHost(resource: Aspire.Hosting.Azure.CosmosDB/Aspire.Hosting.AzureCosmosDBResource|Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.AzureStorageResource|Aspire.Hosting.Azure.Search/Aspire.Hosting.Azure.AzureSearchResource|Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.FoundryResource) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource
Aspire.Hosting.Foundry/withComputeEnvironmentExecutable(project?: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource, configure?: callback) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints
Aspire.Hosting.Foundry/withFoundryDeploymentProperties(configure: callback) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.FoundryDeploymentResource
Aspire.Hosting.Foundry/withFoundryRoleAssignments(target: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.FoundryResource, roles: enum:Aspire.Hosting.FoundryRole[]) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource
Aspire.Hosting.Foundry/withKeyVault(keyVault: Aspire.Hosting.Azure.KeyVault/Aspire.Hosting.Azure.AzureKeyVaultResource) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource
Expand Down
Loading
Loading