From 9ec2aef1288958e0c6b74dd37f6bd9445a298f8b Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 26 Sep 2024 05:29:39 +0000 Subject: [PATCH 1/7] Supporting multiple model definitions against an Ollama resource Fixes #32 --- .../Program.cs | 4 +- .../OllamaResource.cs | 49 +++++++- .../OllamaResourceBuilderExtensions.cs | 113 ++++++++++++------ .../OllamaResourceLifecycleHook.cs | 66 +++++----- .../ResourceCreationTests.cs | 30 ++++- 5 files changed, 185 insertions(+), 77 deletions(-) diff --git a/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs b/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs index e9fcc4e..b05efe9 100644 --- a/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs +++ b/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs @@ -1,6 +1,8 @@ var builder = DistributedApplication.CreateBuilder(args); -var ollama = builder.AddOllama("ollama", modelName: "phi3"); +var ollama = builder.AddOllama("ollama", port: null) + .AddModel("phi3") + .WithDefaultModel("phi3"); builder.AddProject("webfrontend") .WithExternalHttpEndpoints() diff --git a/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResource.cs b/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResource.cs index a045d04..553c52d 100644 --- a/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResource.cs +++ b/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResource.cs @@ -8,13 +8,25 @@ /// /// The name for the resource. /// The LLM to download on initial startup. -public class OllamaResource(string name, string modelName) : ContainerResource(name), IResourceWithConnectionString +public class OllamaResource(string name) : ContainerResource(name), IResourceWithConnectionString { internal const string OllamaEndpointName = "ollama"; + private readonly List _models = []; + + private string? _defaultModel = null; + private EndpointReference? _endpointReference; - public string ModelName { get; internal set; } = modelName; + /// + /// Adds a model to the list of models to download on initial startup. + /// + public IReadOnlyList Models => _models; + + /// + /// The default model to be configured on the Ollama server. + /// + public string? DefaultModel => _defaultModel; /// /// Gets the endpoint for the Ollama server. @@ -28,4 +40,35 @@ public class OllamaResource(string name, string modelName) : ContainerResource(n ReferenceExpression.Create( $"http://{Endpoint.Property(EndpointProperty.Host)}:{Endpoint.Property(EndpointProperty.Port)}" ); -} + + /// + /// Adds a model to the list of models to download on initial startup. + /// + /// The name of the model + public void AddModel(string modelName) + { + ArgumentNullException.ThrowIfNullOrEmpty(modelName, nameof(modelName)); + if (!_models.Contains(modelName)) + { + _models.Add(modelName); + } + } + + /// + /// Sets the default model to be configured on the Ollama server. + /// + /// The name of the model. + /// + /// If the model does not exist in the list of models, it will be added. + /// + public void SetDefaultModel(string modelName) + { + ArgumentNullException.ThrowIfNullOrEmpty(modelName, nameof(modelName)); + _defaultModel = modelName; + + if (!_models.Contains(modelName)) + { + AddModel(modelName); + } + } +} \ No newline at end of file diff --git a/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceBuilderExtensions.cs b/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceBuilderExtensions.cs index 371a163..111f69a 100644 --- a/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceBuilderExtensions.cs +++ b/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceBuilderExtensions.cs @@ -10,41 +10,86 @@ namespace Aspire.Hosting; /// public static class OllamaResourceBuilderExtensions { - /// - /// Adds the Ollama container to the application model. - /// - /// The . - /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. - /// An optional fixed port to bind to the Ollama container. This will be provided randomly by Aspire if not set. - /// The name of the LLM to download on initial startup. llama3 by default. This can be set to null to not download any models. - /// A reference to the . - public static IResourceBuilder AddOllama(this IDistributedApplicationBuilder builder, - string name = "Ollama", int? port = null, string modelName = "llama3") - { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentNullException.ThrowIfNull(name, nameof(name)); - - builder.Services.TryAddLifecycleHook(); - var resource = new OllamaResource(name, modelName); - return builder.AddResource(resource) - .WithAnnotation(new ContainerImageAnnotation { Image = OllamaContainerImageTags.Image, Tag = OllamaContainerImageTags.Tag, Registry = OllamaContainerImageTags.Registry }) - .WithHttpEndpoint(port: port, targetPort: 11434, name: OllamaResource.OllamaEndpointName) - .ExcludeFromManifest(); - } - - /// - /// Adds a data volume to the Ollama container. - /// - /// The . - /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. - /// A flag that indicates if this is a read-only volume. - /// A reference to the . - public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null, bool isReadOnly = false) - { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + /// + /// Adds the Ollama container to the application model. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// An optional fixed port to bind to the Ollama container. This will be provided randomly by Aspire if not set. + /// A reference to the . + public static IResourceBuilder AddOllama(this IDistributedApplicationBuilder builder, string name, int? port = null) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(name, nameof(name)); + + builder.Services.TryAddLifecycleHook(); + var resource = new OllamaResource(name); + return builder.AddResource(resource) + .WithAnnotation(new ContainerImageAnnotation { Image = OllamaContainerImageTags.Image, Tag = OllamaContainerImageTags.Tag, Registry = OllamaContainerImageTags.Registry }) + .WithHttpEndpoint(port: port, targetPort: 11434, name: OllamaResource.OllamaEndpointName) + .ExcludeFromManifest(); + } + + /// + /// Adds the Ollama container to the application model. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// An optional fixed port to bind to the Ollama container. This will be provided randomly by Aspire if not set. + /// The name of the LLM to download on initial startup. llama3 by default. This can be set to null to not download any models. + /// A reference to the . + /// This is to maintain compatibility with the Raygun.Aspire.Hosting.Ollama package and will be removed in the next major release. + [Obsolete("Use AddOllama without a model name, and then the AddModel extension method to add models.")] + public static IResourceBuilder AddOllama(this IDistributedApplicationBuilder builder, + string name = "Ollama", int? port = null, string modelName = "llama3") + { + return builder.AddOllama(name, port) + .AddModel(modelName); + } + + /// + /// Adds a data volume to the Ollama container. + /// + /// The . + /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. + /// A flag that indicates if this is a read-only volume. + /// A reference to the . + public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null, bool isReadOnly = false) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); #pragma warning disable CTASPIRE001 - return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "ollama"), "/root/.ollama", isReadOnly); + return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "ollama"), "/root/.ollama", isReadOnly); #pragma warning restore CTASPIRE001 - } + } + + /// + /// Adds a model to the Ollama container. + /// + /// The . + /// The name of the LLM to download on initial startup. + /// A reference to the . + public static IResourceBuilder AddModel(this IResourceBuilder builder, string modelName) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNullOrEmpty(modelName, nameof(modelName)); + + builder.Resource.AddModel(modelName); + return builder; + } + + /// + /// Sets the default model to be configured on the Ollama server. + /// + /// The . + /// The name of the model. + /// A reference to the . + public static IResourceBuilder WithDefaultModel(this IResourceBuilder builder, string modelName) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNullOrEmpty(modelName, nameof(modelName)); + + builder.Resource.SetDefaultModel(modelName); + return builder; + } } diff --git a/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceLifecycleHook.cs b/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceLifecycleHook.cs index 2201e70..944287b 100644 --- a/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceLifecycleHook.cs +++ b/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceLifecycleHook.cs @@ -33,52 +33,50 @@ public Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, Can private void DownloadModel(OllamaResource resource, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(resource.ModelName)) - { - return; - } - var logger = loggerService.GetLogger(resource); _ = Task.Run(async () => { - try + foreach (string model in resource.Models) { - var connectionString = await resource.ConnectionStringExpression.GetValueAsync(cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrWhiteSpace(connectionString)) + try { - await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot("No connection string", KnownResourceStateStyles.Error) }); - return; - } + var connectionString = await resource.ConnectionStringExpression.GetValueAsync(cancellationToken).ConfigureAwait(false); - var ollamaClient = new OllamaApiClient(new Uri(connectionString)); - var model = resource.ModelName; + if (string.IsNullOrWhiteSpace(connectionString)) + { + await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot("No connection string", KnownResourceStateStyles.Error) }); + return; + } - await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot("Checking model", KnownResourceStateStyles.Info) }); - var hasModel = await HasModelAsync(ollamaClient, model, cancellationToken); + var ollamaClient = new OllamaApiClient(new Uri(connectionString)); - if (!hasModel) - { - logger.LogInformation("{TimeStamp}: [{Model}] needs to be downloaded for {ResourceName}", - DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture), - resource.ModelName, - resource.Name); - await PullModel(resource, ollamaClient, model, logger, cancellationToken); + await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot("Checking model", KnownResourceStateStyles.Info) }); + var hasModel = await HasModelAsync(ollamaClient, model, cancellationToken); + + if (!hasModel) + { + logger.LogInformation("{TimeStamp}: [{Model}] needs to be downloaded for {ResourceName}", + DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture), + model, + resource.Name); + await PullModel(resource, ollamaClient, model, logger, cancellationToken); + } + else + { + logger.LogInformation("{TimeStamp}: [{Model}] already exists for {ResourceName}", + DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture), + model, + resource.Name); + } + + await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot("Running", KnownResourceStateStyles.Success) }); } - else + catch (Exception ex) { - logger.LogInformation("{TimeStamp}: [{Model}] already exists for {ResourceName}", - DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture), - resource.ModelName, - resource.Name); + await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot(ex.Message, KnownResourceStateStyles.Error) }); + break; } - - await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot("Running", KnownResourceStateStyles.Success) }); - } - catch (Exception ex) - { - await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot(ex.Message, KnownResourceStateStyles.Error) }); } }, cancellationToken).ConfigureAwait(false); diff --git a/tests/Aspire.CommunityToolkit.Hosting.Ollama.Tests/ResourceCreationTests.cs b/tests/Aspire.CommunityToolkit.Hosting.Ollama.Tests/ResourceCreationTests.cs index c1b7ea9..ea16478 100644 --- a/tests/Aspire.CommunityToolkit.Hosting.Ollama.Tests/ResourceCreationTests.cs +++ b/tests/Aspire.CommunityToolkit.Hosting.Ollama.Tests/ResourceCreationTests.cs @@ -8,7 +8,7 @@ public class ResourceCreationTests public void VerifyDefaultModel() { var builder = DistributedApplication.CreateBuilder(); - builder.AddOllama("ollama"); + builder.AddOllama("ollama", port: null).AddModel("llama3").WithDefaultModel("llama3"); using var app = builder.Build(); @@ -18,14 +18,14 @@ public void VerifyDefaultModel() Assert.Equal("ollama", resource.Name); - Assert.Equal("llama3", resource.ModelName); + Assert.Equal("llama3", resource.DefaultModel); } [Fact] public void VerifyCustomModel() { var builder = DistributedApplication.CreateBuilder(); - builder.AddOllama("ollama", modelName: "custom"); + builder.AddOllama("ollama", port: null).AddModel("custom"); using var app = builder.Build(); @@ -35,14 +35,14 @@ public void VerifyCustomModel() Assert.Equal("ollama", resource.Name); - Assert.Equal("custom", resource.ModelName); + Assert.Contains("custom", resource.Models); } [Fact] public void VerifyDefaultPort() { var builder = DistributedApplication.CreateBuilder(); - builder.AddOllama("ollama"); + builder.AddOllama("ollama", port: null); using var app = builder.Build(); @@ -71,4 +71,24 @@ public void VerifyCustomPort() Assert.Equal(12345, endpoint.Port); } + + [Fact] + public void CanSetMultpleModels() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddOllama("ollama", port: null) + .AddModel("llama3") + .AddModel("phi3"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("ollama", resource.Name); + + Assert.Contains("llama3", resource.Models); + Assert.Contains("phi3", resource.Models); + } } From fe851826d932e72aa11e9fb581a361de0b9abcdc Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 26 Sep 2024 21:56:13 +1000 Subject: [PATCH 2/7] experimenting with running multiple modes Fixing conditional tests --- Directory.Packages.props | 3 ++- .../Program.cs | 1 + .../Aspire.CommunityToolkit.Testing.csproj | 11 +++++++++++ .../XUnit/ConditionalFactAttribute.cs | 2 +- .../XUnit/ConditionalTheoryAttribute.cs | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4b09baf..2ec3641 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,8 +26,9 @@ - + + diff --git a/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs b/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs index b05efe9..0a8c8d6 100644 --- a/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs +++ b/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs @@ -2,6 +2,7 @@ var ollama = builder.AddOllama("ollama", port: null) .AddModel("phi3") + .AddModel("gemma2:2b") .WithDefaultModel("phi3"); builder.AddProject("webfrontend") diff --git a/tests/Aspire.CommunityToolkit.Testing/Aspire.CommunityToolkit.Testing.csproj b/tests/Aspire.CommunityToolkit.Testing/Aspire.CommunityToolkit.Testing.csproj index fa71b7a..73b959d 100644 --- a/tests/Aspire.CommunityToolkit.Testing/Aspire.CommunityToolkit.Testing.csproj +++ b/tests/Aspire.CommunityToolkit.Testing/Aspire.CommunityToolkit.Testing.csproj @@ -6,4 +6,15 @@ enable + + + + diff --git a/tests/Aspire.CommunityToolkit.Testing/XUnit/ConditionalFactAttribute.cs b/tests/Aspire.CommunityToolkit.Testing/XUnit/ConditionalFactAttribute.cs index 92077e2..4b5edce 100644 --- a/tests/Aspire.CommunityToolkit.Testing/XUnit/ConditionalFactAttribute.cs +++ b/tests/Aspire.CommunityToolkit.Testing/XUnit/ConditionalFactAttribute.cs @@ -10,7 +10,7 @@ namespace Microsoft.TestUtilities; [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -[XunitTestCaseDiscoverer("Microsoft.TestUtilities." + nameof(ConditionalFactDiscoverer), "Microsoft.TestUtilities")] +[XunitTestCaseDiscoverer("Microsoft.TestUtilities." + nameof(ConditionalFactDiscoverer), "Aspire.CommunityToolkit.Testing")] public class ConditionalFactAttribute : FactAttribute { } diff --git a/tests/Aspire.CommunityToolkit.Testing/XUnit/ConditionalTheoryAttribute.cs b/tests/Aspire.CommunityToolkit.Testing/XUnit/ConditionalTheoryAttribute.cs index d5f2306..95401a9 100644 --- a/tests/Aspire.CommunityToolkit.Testing/XUnit/ConditionalTheoryAttribute.cs +++ b/tests/Aspire.CommunityToolkit.Testing/XUnit/ConditionalTheoryAttribute.cs @@ -10,7 +10,7 @@ namespace Microsoft.TestUtilities; [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -[XunitTestCaseDiscoverer("Microsoft.TestUtilities." + nameof(ConditionalTheoryDiscoverer), "Microsoft.TestUtilities")] +[XunitTestCaseDiscoverer("Microsoft.TestUtilities." + nameof(ConditionalTheoryDiscoverer), "Aspire.CommunityToolkit.Testing")] public class ConditionalTheoryAttribute : TheoryAttribute { } From 35030a7b3fc954656474780ac40d77c80414f0fd Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 27 Sep 2024 00:25:40 +0000 Subject: [PATCH 3/7] Little tweak to the messaging in the UI --- .../Program.cs | 1 - .../OllamaResourceLifecycleHook.cs | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs b/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs index 0a8c8d6..b05efe9 100644 --- a/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs +++ b/examples/ollama/Aspire.CommunityToolkit.Hosting.Ollama.AppHost/Program.cs @@ -2,7 +2,6 @@ var ollama = builder.AddOllama("ollama", port: null) .AddModel("phi3") - .AddModel("gemma2:2b") .WithDefaultModel("phi3"); builder.AddProject("webfrontend") diff --git a/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceLifecycleHook.cs b/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceLifecycleHook.cs index 944287b..b16d8c8 100644 --- a/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceLifecycleHook.cs +++ b/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceLifecycleHook.cs @@ -51,7 +51,7 @@ private void DownloadModel(OllamaResource resource, CancellationToken cancellati var ollamaClient = new OllamaApiClient(new Uri(connectionString)); - await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot("Checking model", KnownResourceStateStyles.Info) }); + await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot($"Checking {model}", KnownResourceStateStyles.Info) }); var hasModel = await HasModelAsync(ollamaClient, model, cancellationToken); if (!hasModel) @@ -108,7 +108,7 @@ private async Task PullModel(OllamaResource resource, OllamaApiClient ollamaClie logger.LogInformation("{TimeStamp}: Pulling ollama model {Model}...", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture), model); - await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot("Downloading model", KnownResourceStateStyles.Info) }); + await _notificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot($"Downloading {model}", KnownResourceStateStyles.Info) }); long percentage = 0; @@ -126,7 +126,7 @@ private async Task PullModel(OllamaResource resource, OllamaApiClient ollamaClie { percentage = newPercentage; - var percentageState = percentage == 0 ? "Downloading model" : $"Downloading model {percentage} percent"; + var percentageState = $"Downloading {model}{(percentage > 0 ? $" {percentage} percent" : "")}"; await _notificationService.PublishUpdateAsync(resource, state => state with { From a67149b600ae7637cdb018416f7baa79bee56854 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 27 Sep 2024 00:29:09 +0000 Subject: [PATCH 4/7] Little docs update --- docs/integrations/hosting-ollama.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/integrations/hosting-ollama.md b/docs/integrations/hosting-ollama.md index 7d3a77b..b7da55c 100644 --- a/docs/integrations/hosting-ollama.md +++ b/docs/integrations/hosting-ollama.md @@ -13,21 +13,20 @@ Use the static `AddOllama` method to add this container component to the applica ```csharp // The distributed application builder is created here -var ollama = builder.AddOllama("ollama"); +var ollama = builder.AddOllama("ollama").AddModel("llama3"); // The builder is used to build and run the app somewhere down here ``` ### Configuration -The AddOllama method has optional arguments to set the `name`, `port` and `modelName`. +The AddOllama method has optional arguments to set the `name` and `port`. The `name` is what gets displayed in the Aspire orchestration app against this component. The `port` is provided randomly by Aspire. If for whatever reason you need a fixed port, you can set that here. -The `modelName` specifies what LLM to pull when it starts up. The default is `llama3`. You can also set this to null to prevent any models being pulled on startup - leaving you with a plain Ollama container to work with. ## Downloading the LLM -When the Ollama container for this component first spins up, this component will download the LLM (llama3 unless otherwise specified). +When the Ollama container for this component first spins up, this component will download the LLM(s). The progress of this download will be displayed in the State column for this component on the Aspire orchestration app. Important: Keep the Aspire orchestration app open until the download is complete, otherwise the download will be cancelled. In the spirit of productivity, we recommend kicking off this process before heading for lunch. @@ -45,8 +44,7 @@ Within that component (e.g. a web app), you can fetch the Ollama connection stri Note that if you changed the name of the Ollama component via the `name` argument, then you'll need to use that here when specifying which connection string to get. ```csharp -var connectionString = builder.Configuration.GetConnectionString("Ollama"); +var connectionString = builder.Configuration.GetConnectionString("ollama"); ``` You can then call any of the Ollama endpoints through this connection string. We recommend using the [OllamaSharp](https://www.nuget.org/packages/OllamaSharp) client to do this. - From ca09b6abd110e4c43436b905f2384b378fc03bad Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 27 Sep 2024 00:45:28 +0000 Subject: [PATCH 5/7] Better naming of test output --- Directory.Build.targets | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Directory.Build.targets diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..96ce250 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,7 @@ + + + + trx%3BLogFileName=$(AssemblyName)-$(TargetFramework).trx + + + \ No newline at end of file From 1e9461d24aeef248af32751df4586c1da719e4c2 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 27 Sep 2024 04:29:34 +0000 Subject: [PATCH 6/7] More tests for Ollama --- .../OllamaResourceBuilderExtensions.cs | 4 +- .../ResourceCreationTests.cs | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceBuilderExtensions.cs b/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceBuilderExtensions.cs index 111f69a..12429a5 100644 --- a/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceBuilderExtensions.cs +++ b/src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResourceBuilderExtensions.cs @@ -72,7 +72,7 @@ public static IResourceBuilder WithDataVolume(this IResourceBuil public static IResourceBuilder AddModel(this IResourceBuilder builder, string modelName) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentNullException.ThrowIfNullOrEmpty(modelName, nameof(modelName)); + ArgumentException.ThrowIfNullOrWhiteSpace(modelName, nameof(modelName)); builder.Resource.AddModel(modelName); return builder; @@ -87,7 +87,7 @@ public static IResourceBuilder AddModel(this IResourceBuilder
    WithDefaultModel(this IResourceBuilder builder, string modelName) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentNullException.ThrowIfNullOrEmpty(modelName, nameof(modelName)); + ArgumentException.ThrowIfNullOrWhiteSpace(modelName, nameof(modelName)); builder.Resource.SetDefaultModel(modelName); return builder; diff --git a/tests/Aspire.CommunityToolkit.Hosting.Ollama.Tests/ResourceCreationTests.cs b/tests/Aspire.CommunityToolkit.Hosting.Ollama.Tests/ResourceCreationTests.cs index ea16478..f898be0 100644 --- a/tests/Aspire.CommunityToolkit.Hosting.Ollama.Tests/ResourceCreationTests.cs +++ b/tests/Aspire.CommunityToolkit.Hosting.Ollama.Tests/ResourceCreationTests.cs @@ -91,4 +91,58 @@ public void CanSetMultpleModels() Assert.Contains("llama3", resource.Models); Assert.Contains("phi3", resource.Models); } + + [Fact] + public void DefaultModelAddedToModelList() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddOllama("ollama", port: null).AddModel("llama3").WithDefaultModel("llama3"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("ollama", resource.Name); + + Assert.Single(resource.Models); + Assert.Contains("llama3", resource.Models); + } + + [Fact] + public void DistributedApplicationBuilderCannotBeNull() + { + Assert.Throws(() => DistributedApplication.CreateBuilder().AddOllama(null!, port: null)); + } + + [Fact] + public void ResourceNameCannotBeOmitted() + { + Assert.Throws(() => DistributedApplication.CreateBuilder().AddOllama("", port: null)); + Assert.Throws(() => DistributedApplication.CreateBuilder().AddOllama(" ", port: null)); + Assert.Throws(() => DistributedApplication.CreateBuilder().AddOllama(null!, port: null)); + } + + [Fact] + public void ModelNameCannotBeOmmitted() + { + var builder = DistributedApplication.CreateBuilder(); + var ollama = builder.AddOllama("ollama", port: null); + + Assert.Throws(() => ollama.AddModel("")); + Assert.Throws(() => ollama.AddModel(" ")); + Assert.Throws(() => ollama.AddModel(null!)); + } + + [Fact] + public void DefaultModelCannotBeOmitted() + { + var builder = DistributedApplication.CreateBuilder(); + var ollama = builder.AddOllama("ollama", port: null); + + Assert.Throws(() => ollama.WithDefaultModel("")); + Assert.Throws(() => ollama.WithDefaultModel(" ")); + Assert.Throws(() => ollama.WithDefaultModel(null!)); + } } From 4c4c6486080524468b66cb0d91d70b6dc207c23b Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Fri, 27 Sep 2024 05:00:59 +0000 Subject: [PATCH 7/7] Combining tests to see if this makes them more consistent --- .../JavaHostingComponentTests.cs | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/tests/Aspire.CommunityToolkit.Hosting.Java.Tests/JavaHostingComponentTests.cs b/tests/Aspire.CommunityToolkit.Hosting.Java.Tests/JavaHostingComponentTests.cs index 610da78..1c8b1e7 100644 --- a/tests/Aspire.CommunityToolkit.Hosting.Java.Tests/JavaHostingComponentTests.cs +++ b/tests/Aspire.CommunityToolkit.Hosting.Java.Tests/JavaHostingComponentTests.cs @@ -7,25 +7,12 @@ namespace Aspire.CommunityToolkit.Hosting.Java.Tests; #pragma warning disable CTASPIRE001 public class JavaHostingComponentTests(AspireIntegrationTestFixture fixture) : IClassFixture> { - [ConditionalFact] + [ConditionalTheory] [OSSkipCondition(OperatingSystems.Windows)] - public async Task ContainerAppResourceWillRespondWithOk() + [InlineData("containerapp")] + [InlineData("executableapp")] + public async Task AppResourceWillRespondWithOk(string resourceName) { - var resourceName = "containerapp"; - var httpClient = fixture.CreateHttpClient(resourceName); - - await fixture.App.WaitForTextAsync("Started SpringMavenApplication", resourceName).WaitAsync(TimeSpan.FromMinutes(5)); - - var response = await httpClient.GetAsync("/"); - - response.StatusCode.Should().Be(HttpStatusCode.OK); - } - - [ConditionalFact] - [OSSkipCondition(OperatingSystems.Windows)] - public async Task ExecutableAppResourceWillRespondWithOk() - { - var resourceName = "executableapp"; var httpClient = fixture.CreateHttpClient(resourceName); await fixture.App.WaitForTextAsync("Started SpringMavenApplication", resourceName).WaitAsync(TimeSpan.FromMinutes(5));