Initial draft of actor runtime abstractions (#197)

* Initial draft of actor runtime abstractions
This commit is contained in:
Reuben Bond
2025-07-22 13:05:58 -07:00
committed by GitHub
Unverified
parent 8f2d3da80d
commit 41d441420e
108 changed files with 7445 additions and 58 deletions
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.3.1" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>2969a84d-8ee6-4304-8737-6e469a315aa8</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="Aspire.Hosting.Azure.CognitiveServices" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HelloHttpApi.ApiService\HelloHttpApi.ApiService.csproj" />
<ProjectReference Include="..\HelloHttpApi.Web\HelloHttpApi.Web.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,266 @@
// Copyright (c) Microsoft. All rights reserved.
namespace HelloHttpApi.AppHost;
public static class ModelExtensions
{
public static IResourceBuilder<AIModel> AddAIModel(this IDistributedApplicationBuilder builder, string name)
{
var model = new AIModel(name);
return builder.CreateResourceBuilder(model);
}
public static IResourceBuilder<AIModel> RunAsOpenAI(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> apiKey)
{
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
return builder.AsOpenAI(modelName, apiKey);
}
return builder;
}
public static IResourceBuilder<AIModel> PublishAsOpenAI(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> apiKey)
{
if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
{
return builder.AsOpenAI(modelName, apiKey);
}
return builder;
}
public static IResourceBuilder<AIModel> RunAsAzureOpenAI(this IResourceBuilder<AIModel> builder, string modelName, Action<IResourceBuilder<AzureOpenAIResource>>? configure)
{
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
return builder.AsAzureOpenAI(modelName, configure);
}
return builder;
}
public static IResourceBuilder<AIModel> PublishAsAzureOpenAI(this IResourceBuilder<AIModel> builder, string modelName, Action<IResourceBuilder<AzureOpenAIResource>>? configure)
{
if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
{
return builder.AsAzureOpenAI(modelName, configure);
}
return builder;
}
public static IResourceBuilder<AIModel> AsAzureOpenAI(this IResourceBuilder<AIModel> builder, string modelName, Action<IResourceBuilder<AzureOpenAIResource>>? configure)
{
builder.Reset();
var openAIModel = builder.ApplicationBuilder.AddAzureOpenAI(builder.Resource.Name);
configure?.Invoke(openAIModel);
builder.Resource.UnderlyingResource = openAIModel.Resource;
// Add the model name to the connection string
builder.Resource.ConnectionString = ReferenceExpression.Create($"{openAIModel.Resource.ConnectionStringExpression};Model={modelName}");
builder.Resource.Provider = "AzureOpenAI";
return builder;
}
public static IResourceBuilder<AIModel> RunAsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> endpoint, IResourceBuilder<ParameterResource> apiKey)
{
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
return builder.AsAzureAIInference(modelName, endpoint, apiKey);
}
return builder;
}
public static IResourceBuilder<AIModel> PublishAsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> endpoint, IResourceBuilder<ParameterResource> apiKey)
{
if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
{
return builder.AsAzureAIInference(modelName, endpoint, apiKey);
}
return builder;
}
public static IResourceBuilder<AIModel> AsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> endpoint, IResourceBuilder<ParameterResource> apiKey)
{
builder.Reset();
// See: https://github.com/dotnet/aspire/issues/7641
var csb = new ReferenceExpressionBuilder();
csb.Append($"Endpoint={endpoint.Resource};");
csb.Append($"AccessKey={apiKey.Resource};");
csb.Append($"Model={modelName}");
var cs = csb.Build();
builder.ApplicationBuilder.AddResource(builder.Resource);
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
var csTask = cs.GetValueAsync(default).AsTask();
if (!csTask.IsCompletedSuccessfully)
{
throw new InvalidOperationException("Connection string could not be resolved!");
}
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
builder.WithInitialState(new CustomResourceSnapshot
{
ResourceType = "Azure AI Inference Model",
State = KnownResourceStates.Running,
Properties = [
new("ConnectionString", csTask.Result ) { IsSensitive = true }
]
});
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}
builder.Resource.UnderlyingResource = builder.Resource;
builder.Resource.ConnectionString = cs;
builder.Resource.Provider = "AzureAIInference";
return builder;
}
public static IResourceBuilder<AIModel> RunAsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, string endpoint, IResourceBuilder<ParameterResource> apiKey)
{
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
return builder.AsAzureAIInference(modelName, endpoint, apiKey);
}
return builder;
}
public static IResourceBuilder<AIModel> PublishAsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, string endpoint, IResourceBuilder<ParameterResource> apiKey)
{
if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
{
return builder.AsAzureAIInference(modelName, endpoint, apiKey);
}
return builder;
}
public static IResourceBuilder<AIModel> AsAzureAIInference(this IResourceBuilder<AIModel> builder, string modelName, string endpoint, IResourceBuilder<ParameterResource> apiKey)
{
builder.Reset();
// See: https://github.com/dotnet/aspire/issues/7641
var csb = new ReferenceExpressionBuilder();
csb.Append($"Endpoint={endpoint};");
csb.Append($"AccessKey={apiKey.Resource};");
csb.Append($"Model={modelName}");
var cs = csb.Build();
builder.ApplicationBuilder.AddResource(builder.Resource);
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
var csTask = cs.GetValueAsync(default).AsTask();
if (!csTask.IsCompletedSuccessfully)
{
throw new InvalidOperationException("Connection string could not be resolved!");
}
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
builder.WithInitialState(new CustomResourceSnapshot
{
ResourceType = "Azure AI Inference Model",
State = KnownResourceStates.Running,
Properties = [
new("ConnectionString", csTask.Result ) { IsSensitive = true }
]
});
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}
builder.Resource.UnderlyingResource = builder.Resource;
builder.Resource.ConnectionString = cs;
builder.Resource.Provider = "AzureAIInference";
return builder;
}
public static IResourceBuilder<AIModel> AsOpenAI(this IResourceBuilder<AIModel> builder, string modelName, IResourceBuilder<ParameterResource> apiKey)
{
builder.Reset();
// See: https://github.com/dotnet/aspire/issues/7641
var csb = new ReferenceExpressionBuilder();
csb.Append($"AccessKey={apiKey.Resource};");
csb.Append($"Model={modelName}");
var cs = csb.Build();
builder.ApplicationBuilder.AddResource(builder.Resource);
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
var csTask = cs.GetValueAsync(default).AsTask();
if (!csTask.IsCompletedSuccessfully)
{
throw new InvalidOperationException("Connection string could not be resolved!");
}
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
builder.WithInitialState(new CustomResourceSnapshot
{
ResourceType = "OpenAI Model",
State = KnownResourceStates.Running,
Properties = [
new("ConnectionString", csTask.Result ) { IsSensitive = true }
]
});
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}
builder.Resource.UnderlyingResource = builder.Resource;
builder.Resource.ConnectionString = cs;
builder.Resource.Provider = "OpenAI";
return builder;
}
private static void Reset(this IResourceBuilder<AIModel> builder)
{
// Reset the properties of the AIModel resource
if (builder.Resource.UnderlyingResource is { } underlyingResource)
{
builder.ApplicationBuilder.Resources.Remove(underlyingResource);
if (underlyingResource is IResourceWithParent resourceWithParent)
{
builder.ApplicationBuilder.Resources.Remove(resourceWithParent.Parent);
}
}
builder.Resource.ConnectionString = null;
builder.Resource.Provider = null;
}
}
// A resource representing an AI model.
public class AIModel(string name) : Resource(name), IResourceWithConnectionString
{
internal string? Provider { get; set; }
internal IResourceWithConnectionString? UnderlyingResource { get; set; }
internal ReferenceExpression? ConnectionString { get; set; }
public ReferenceExpression ConnectionStringExpression =>
this.Build();
public ReferenceExpression Build()
{
var connectionString = this.ConnectionString ?? throw new InvalidOperationException("No connection string available.");
if (this.Provider is null)
{
throw new InvalidOperationException("No provider configured.");
}
return ReferenceExpression.Create($"{connectionString};Provider={this.Provider}");
}
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using HelloHttpApi.AppHost;
var builder = DistributedApplication.CreateBuilder(args);
var azOpenAiResource = builder.AddParameterFromConfiguration("AzureOpenAIName", "AzureOpenAI:Name");
var azOpenAiResourceGroup = builder.AddParameterFromConfiguration("AzureOpenAIResourceGroup", "AzureOpenAI:ResourceGroup");
var chatModel = builder.AddAIModel("chat-model").AsAzureOpenAI("gpt-4o", o => o.AsExisting(azOpenAiResource, azOpenAiResourceGroup));
var apiService = builder.AddProject<Projects.HelloHttpApi_ApiService>("apiservice")
.WithReference(chatModel);
builder.AddProject<Projects.HelloHttpApi_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(apiService)
.WaitFor(apiService);
builder.Build().Run();
@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17277;http://localhost:15143",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21000",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22278"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15143",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19242",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20010"
}
}
}
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft.AspNetCore": "Warning"
}
}
}
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}