Skip to content

Commit

Permalink
On localhost launch all .NET + python examples + assistants in Aspire (
Browse files Browse the repository at this point in the history
…#272)

* When running Aspire locally, load all .NET agent examples, all python
examples, all python assistants
* Fix Aspire `uv` extension not to change `uv.lock` files
* Automatically open Aspire dashboard in browser
* Improve connector ping logic


![image](https://github.com/user-attachments/assets/1c0fd578-97ba-473f-b46a-aa365c2cd723)


![image](https://github.com/user-attachments/assets/b11b6fa7-c67b-4928-9bb0-45ba80aca9e6)
  • Loading branch information
dluc authored Dec 6, 2024
1 parent 24b3519 commit 47cdd69
Show file tree
Hide file tree
Showing 17 changed files with 218 additions and 86 deletions.
5 changes: 3 additions & 2 deletions aspire-orchestrator/Aspire.AppHost/Aspire.AppHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
</ItemGroup>

<ItemGroup>
<ProjectReference
Include="..\..\examples\dotnet\dotnet-03-simple-chatbot\dotnet-03-simple-chatbot.csproj" />
<ProjectReference Include="..\..\examples\dotnet\dotnet-01-echo-bot\dotnet-01-echo-bot.csproj" />
<ProjectReference Include="..\..\examples\dotnet\dotnet-02-message-types-demo\dotnet-02-message-types-demo.csproj" />
<ProjectReference Include="..\..\examples\dotnet\dotnet-03-simple-chatbot\dotnet-03-simple-chatbot.csproj" />
<ProjectReference Include="..\Aspire.Extensions\Aspire.Extensions.csproj" IsAspireProjectResource="false" />
</ItemGroup>

Expand Down
148 changes: 113 additions & 35 deletions aspire-orchestrator/Aspire.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,39 +1,117 @@
// Copyright (c) Microsoft. All rights reserved.

var builder = DistributedApplication.CreateBuilder(args);

var authority = builder.AddParameterFromConfiguration("authority", "EntraId:Authority");
var clientId = builder.AddParameterFromConfiguration("clientId", "EntraId:ClientId");

// Workbench backend
var workbenchService = builder.AddWorkbenchService("workbenchservice", projectDirectory: Path.Combine("..", "..", "workbench-service"), clientId: clientId);
var workbenchServiceEndpoint = workbenchService.GetSemanticWorkbenchEndpoint(builder.ExecutionContext.IsPublishMode);

// Workbench frontend
var workbenchApp = builder.AddViteApp("workbenchapp", workingDirectory: Path.Combine("..", "..", "workbench-app"), packageManager: "pnpm")
.WithPnpmPackageInstallation()
.WithEnvironment(name: "VITE_SEMANTIC_WORKBENCH_SERVICE_URL", workbenchServiceEndpoint)
.WaitFor(workbenchService)
.PublishAsDockerFile([
new DockerBuildArg("VITE_SEMANTIC_WORKBENCH_CLIENT_ID", clientId.Resource.Value),
new DockerBuildArg("VITE_SEMANTIC_WORKBENCH_AUTHORITY", authority.Resource.Value),
]);

// Sample Python agent
builder.AddAssistantApp("skill-assistant", projectDirectory: Path.Combine("..", "..", "assistants", "skill-assistant"), assistantModuleName: "skill-assistant")
.WithEnvironment(name: "assistant__workbench_service_url", workbenchServiceEndpoint);

// Sample .NET agent
var simpleChatBot = builder.AddProject<Projects.dotnet_03_simple_chatbot>("simple-chatbot-dotnet")
.WithHttpEndpoint()
.WaitFor(workbenchService)
.WithEnvironment(name: "Workbench__WorkbenchEndpoint", workbenchServiceEndpoint);

simpleChatBot.WithEnvironment("Workbench__ConnectorHost", $"{simpleChatBot.GetEndpoint("http")}");

if (!builder.ExecutionContext.IsPublishMode)
using Aspire.Hosting.Extensions;
using Projects;

internal static class Program
{
workbenchApp.WithHttpsEndpoint(env: "PORT");
}
public static void Main(string[] args)
{
var builder = DistributedApplication.CreateBuilder(args);

builder
.AddSemanticWorkbench(out IResourceBuilder<ExecutableResource> workbenchBackend, out EndpointReference workbenchEndpoint)
.AddPythonAssistant("skill-assistant", workbenchEndpoint)
.AddDotnetExample<dotnet_03_simple_chatbot>("simple-chatbot-dotnet", workbenchBackend, workbenchEndpoint);

// When running locally
if (!builder.ExecutionContext.IsPublishMode)
{
builder
.AddPythonExample("python-01-echo-bot", workbenchEndpoint)
.AddPythonExample("python-02-simple-chatbot", workbenchEndpoint)
.AddPythonExample("python-03-multimodel-chatbot", workbenchEndpoint)
.AddPythonAssistant("explorer-assistant", workbenchEndpoint)
.AddPythonAssistant("guided-conversation-assistant", workbenchEndpoint)
.AddPythonAssistant("prospector-assistant", workbenchEndpoint)
.AddDotnetExample<dotnet_01_echo_bot>("echo-bot-dotnet", workbenchBackend, workbenchEndpoint)
.AddDotnetExample<dotnet_02_message_types_demo>("sw-tutorial-bot-dotnet", workbenchBackend, workbenchEndpoint);
}

builder
.ShowDashboardUrl(true)
.LaunchDashboard(delay: 5000)
.Build()
.Run();
}

/// <summary>
/// Add the workbench frontend and backend components
/// </summary>
private static IDistributedApplicationBuilder AddSemanticWorkbench(this IDistributedApplicationBuilder builder,
out IResourceBuilder<ExecutableResource> workbenchBackend, out EndpointReference workbenchServiceEndpoint)
{
var authority = builder.AddParameterFromConfiguration("authority", "EntraId:Authority");
var clientId = builder.AddParameterFromConfiguration("clientId", "EntraId:ClientId");

// Workbench backend
workbenchBackend = builder.AddWorkbenchService(
name: "workbenchservice",
projectDirectory: Path.Combine("..", "..", "workbench-service"),
clientId: clientId);

workbenchServiceEndpoint = workbenchBackend.GetSemanticWorkbenchEndpoint(builder.ExecutionContext.IsPublishMode);

// Workbench frontend
var workbenchApp = builder.AddViteApp(
name: "workbenchapp",
workingDirectory: Path.Combine("..", "..", "workbench-app"),
packageManager: "pnpm")
.WithPnpmPackageInstallation()
.WithEnvironment(name: "VITE_SEMANTIC_WORKBENCH_SERVICE_URL", workbenchServiceEndpoint)
.WaitFor(workbenchBackend)
.PublishAsDockerFile([
new DockerBuildArg("VITE_SEMANTIC_WORKBENCH_CLIENT_ID", clientId.Resource.Value),
new DockerBuildArg("VITE_SEMANTIC_WORKBENCH_AUTHORITY", authority.Resource.Value),
]);

builder.Build().Run();
// When running locally
if (!builder.ExecutionContext.IsPublishMode)
{
workbenchApp.WithHttpsEndpoint(env: "PORT");
}

return builder;
}

private static IDistributedApplicationBuilder AddPythonAssistant(this IDistributedApplicationBuilder builder,
string name, EndpointReference workbenchServiceEndpoint)
{
builder
.AddAssistantUvPythonApp(
name: name,
projectDirectory: Path.Combine("..", "..", "assistants", name),
assistantModuleName: name)
.WithEnvironment(name: "assistant__workbench_service_url", workbenchServiceEndpoint);

return builder;
}

private static IDistributedApplicationBuilder AddPythonExample(this IDistributedApplicationBuilder builder,
string name, EndpointReference workbenchServiceEndpoint)
{
builder
.AddAssistantUvPythonApp(
name: name,
projectDirectory: Path.Combine("..", "..", "examples", "python", name),
assistantModuleName: "assistant")
.WithEnvironment(name: "assistant__workbench_service_url", workbenchServiceEndpoint);

return builder;
}

// .NET Agent Example 1
private static IDistributedApplicationBuilder AddDotnetExample<T>(this IDistributedApplicationBuilder builder,
string name, IResourceBuilder<ExecutableResource> workbenchBackend, EndpointReference workbenchServiceEndpoint) where T : IProjectMetadata, new()
{
var agent = builder
.AddProject<T>(name: name)
.WithHttpEndpoint()
.WaitFor(workbenchBackend)
.WithEnvironment(name: "Workbench__WorkbenchEndpoint", workbenchServiceEndpoint);

agent.WithEnvironment("Workbench__ConnectorHost", $"{agent.GetEndpoint("http")}");

return builder;
}
}
40 changes: 40 additions & 0 deletions aspire-orchestrator/Aspire.Extensions/Dashboard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics;

namespace Aspire.Hosting.Extensions;

public static class Dashboard
{
/// <summary>
/// Show Aspire dashboard URL before the logging start.
/// </summary>
public static IDistributedApplicationBuilder ShowDashboardUrl(this IDistributedApplicationBuilder builder, bool withStyle = false)
{
Console.WriteLine(withStyle
? $"\u001b[1mAspire dashboard URL: {GetUrl(builder)}\u001b[0m\n\n"
: $"Aspire dashboard URL: {GetUrl(builder)}\n\n");
return builder;
}

/// <summary>
/// Wait 5 seconds and automatically open the browser (when using 'dotnet run' the browser doesn't open)
/// </summary>
public static IDistributedApplicationBuilder LaunchDashboard(this IDistributedApplicationBuilder builder, int delay = 5000)
{
Task.Run(async () =>
{
await Task.Delay(delay).ConfigureAwait(false);
Process.Start(new ProcessStartInfo { FileName = GetUrl(builder), UseShellExecute = true });
});

return builder;
}

private static string GetUrl(IDistributedApplicationBuilder builder)
{
string token = builder.Configuration["AppHost:BrowserToken"] ?? string.Empty;
string url = builder.Configuration["ASPNETCORE_URLS"]?.Split(";")[0] ?? throw new ArgumentException("ASPNETCORE_URLS is empty");
return $"{url}/login?t={token}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static IResourceBuilder<UvAppResource> AddUvApp(
private static IResourceBuilder<UvAppResource> AddUvApp(this IDistributedApplicationBuilder builder,
string name,
string scriptPath,
string projectDirectory,
string? projectDirectory,
string virtualEnvironmentPath,
params string[] args)
{
Expand All @@ -32,7 +32,7 @@ private static IResourceBuilder<UvAppResource> AddUvApp(this IDistributedApplica

string wd = projectDirectory ?? Path.Combine("..", name);

projectDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, wd));
projectDirectory = Path.Combine(builder.AppHostDirectory, wd).NormalizePathForCurrentPlatform();

var virtualEnvironment = new VirtualEnvironment(Path.IsPathRooted(virtualEnvironmentPath)
? virtualEnvironmentPath
Expand All @@ -43,8 +43,8 @@ private static IResourceBuilder<UvAppResource> AddUvApp(this IDistributedApplica
// var projectExecutable = instrumentationExecutable ?? pythonExecutable;

string[] allArgs = args is { Length: > 0 }
? ["run", scriptPath, .. args]
: ["run", scriptPath];
? ["run", "--frozen", scriptPath, .. args]
: ["run", "--frozen", scriptPath];

var projectResource = new UvAppResource(name, projectDirectory);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,22 @@ public static IResourceBuilder<ExecutableResource> AddWorkbenchService(
{
ArgumentNullException.ThrowIfNull(builder);

var workbenchService = builder.AddUvApp(name, projectDirectory, "start-semantic-workbench-service", scriptArgs)
var workbenchService = builder
.AddUvApp(name, projectDirectory, "start-semantic-workbench-service", scriptArgs)
.PublishAsDockerImage(dockerContext: Path.Combine("..", ".."),
dockerFilePath: Path.Combine("workbench-service", "Dockerfile"),
configure: new(configure => configure
.WithBuildArg("SSHD_ENABLED", "false")))
.WithEnvironment(name: "WORKBENCH__AUTH__ALLOWED_APP_ID", clientId.Resource.Value);

if (builder.ExecutionContext.IsPublishMode)
{
// When running on Azure
workbenchService.WithHttpsEndpoint(port: 3000);
}
else
{
// When running locally
workbenchService.WithHttpEndpoint(env: "PORT");
}

Expand All @@ -40,27 +44,31 @@ public static EndpointReference GetSemanticWorkbenchEndpoint(this IResourceBuild
return workbenchService.GetEndpoint(isPublishMode ? "https" : "http");
}

public static IResourceBuilder<ExecutableResource> AddAssistantApp(
public static IResourceBuilder<ExecutableResource> AddAssistantUvPythonApp(
this IDistributedApplicationBuilder builder,
string name,
string projectDirectory,
string assistantModuleName)
{
ArgumentNullException.ThrowIfNull(builder);

var assistant = builder.AddUvApp(name, projectDirectory, "start-assistant")
var assistant = builder
.AddUvApp(name, projectDirectory, "start-assistant")
.PublishAsDockerImage(dockerContext: Path.Combine("..", ".."),
dockerFilePath: Path.Combine("tools", "docker", "Dockerfile.assistant"),
configure: new(configure => configure
.WithBuildArg("package", assistantModuleName)
.WithBuildArg("app", $"assistant.{assistantModuleName.Replace('-', '_')}:app")
));

if (builder.ExecutionContext.IsPublishMode)
{
// When running on Azure
assistant.WithHttpEndpoint(port: 3001, env: "ASSISTANT__PORT");
}
else
{
// When running locally
assistant.WithHttpEndpoint(env: "ASSISTANT__PORT");
}

Expand Down
12 changes: 12 additions & 0 deletions aspire-orchestrator/SemanticWorkbench.Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-03-simple-chatbot",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Extensions", "Aspire.Extensions\Aspire.Extensions.csproj", "{773468A0-6628-4D14-8EE8-7C3F790B6192}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-01-echo-bot", "..\examples\dotnet\dotnet-01-echo-bot\dotnet-01-echo-bot.csproj", "{9AB1A55C-A7D7-45E4-9C4C-D3F5BBD78D64}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-02-message-types-demo", "..\examples\dotnet\dotnet-02-message-types-demo\dotnet-02-message-types-demo.csproj", "{B604ADC1-7FF3-449A-9BE7-BC553A810ADE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -32,6 +36,14 @@ Global
{773468A0-6628-4D14-8EE8-7C3F790B6192}.Debug|Any CPU.Build.0 = Debug|Any CPU
{773468A0-6628-4D14-8EE8-7C3F790B6192}.Release|Any CPU.ActiveCfg = Release|Any CPU
{773468A0-6628-4D14-8EE8-7C3F790B6192}.Release|Any CPU.Build.0 = Release|Any CPU
{9AB1A55C-A7D7-45E4-9C4C-D3F5BBD78D64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9AB1A55C-A7D7-45E4-9C4C-D3F5BBD78D64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9AB1A55C-A7D7-45E4-9C4C-D3F5BBD78D64}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9AB1A55C-A7D7-45E4-9C4C-D3F5BBD78D64}.Release|Any CPU.Build.0 = Release|Any CPU
{B604ADC1-7FF3-449A-9BE7-BC553A810ADE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B604ADC1-7FF3-449A-9BE7-BC553A810ADE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B604ADC1-7FF3-449A-9BE7-BC553A810ADE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B604ADC1-7FF3-449A-9BE7-BC553A810ADE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
3 changes: 3 additions & 0 deletions aspire-orchestrator/SemanticWorkbench.Aspire.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LIMIT/@EntryValue">512</s:Int64>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LINES/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">Copyright (c) Microsoft. All rights reserved.</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ABC/@EntryIndexedValue">ABC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ACS/@EntryIndexedValue">ACS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AIGPT/@EntryIndexedValue">AIGPT</s:String>
Expand All @@ -100,6 +101,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GPT/@EntryIndexedValue">GPT</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GRPC/@EntryIndexedValue">GRPC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HMAC/@EntryIndexedValue">HMAC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HTML/@EntryIndexedValue">HTML</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HTTP/@EntryIndexedValue">HTTP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IM/@EntryIndexedValue">IM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IO/@EntryIndexedValue">IO</s:String>
Expand Down Expand Up @@ -267,6 +269,7 @@ public void It$SOMENAME$()
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mirostat/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mixtral/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=msword/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=multimodel/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=myfile/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mypipelinestep/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Notegen/@EntryIndexedValue">True</s:Boolean>
Expand Down
2 changes: 1 addition & 1 deletion aspire-orchestrator/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ fi

cd Aspire.AppHost

dotnet run
dotnet run --launch-profile https
2 changes: 1 addition & 1 deletion examples/dotnet/dotnet-02-message-types-demo/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private static IServiceCollection AddAzureAIContentSafety(
IConfiguration config)
{
var authType = config.GetValue<string>("AuthType");
var endpoint = new Uri(config.GetValue<string>("Endpoint")!);
var endpoint = new Uri(config.GetValue<string>("Endpoint")!) ?? throw new ArgumentException("Failed to set Azure AI Content Safety Endpoint");
var apiKey = config.GetValue<string>("ApiKey");

return services.AddSingleton<ContentSafetyClient>(_ => authType == "AzureIdentity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<RootNamespace>AgentExample</RootNamespace>
<PackageId>AgentExample</PackageId>
<NoWarn>$(NoWarn);CA1308;CA1861;CA1515;IDE0290;</NoWarn>
<NoWarn>$(NoWarn);CA1308;CA1861;CA1515;IDE0290;CA1508;</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion examples/dotnet/dotnet-03-simple-chatbot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private static IServiceCollection AddAzureAIContentSafety(
IConfiguration config)
{
var authType = config.GetValue<string>("AuthType");
var endpoint = new Uri(config.GetValue<string>("Endpoint")!);
var endpoint = new Uri(config.GetValue<string>("Endpoint")!) ?? throw new ArgumentException("Failed to set Azure AI Content Safety Endpoint");
var apiKey = config.GetValue<string>("ApiKey");

return services.AddSingleton<ContentSafetyClient>(_ => authType == "AzureIdentity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<RootNamespace>AgentExample</RootNamespace>
<PackageId>AgentExample</PackageId>
<NoWarn>$(NoWarn);SKEXP0010;CA1861;CA1515;IDE0290;CA1031;CA1812;</NoWarn>
<NoWarn>$(NoWarn);SKEXP0010;CA1861;CA1515;IDE0290;CA1031;CA1812;CA1508;</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit 47cdd69

Please sign in to comment.