Skip to content

Commit

Permalink
Merge pull request #40 from CommunityToolkit/aaronpowell/issue35
Browse files Browse the repository at this point in the history
OpenWebUI for Ollama
  • Loading branch information
aaronpowell authored Sep 30, 2024
2 parents 60c72e4 + 3d2541c commit 63547b3
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

var ollama = builder.AddOllama("ollama", port: null)
.AddModel("phi3")
.WithDefaultModel("phi3");
.WithDefaultModel("phi3")
.WithOpenWebUI();

builder.AddProject<Projects.Aspire_CommunityToolkit_Hosting_Ollama_Web>("webfrontend")
.WithExternalHttpEndpoints()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OllamaSharp"/>
<PackageReference Include="OllamaSharp" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ internal static class OllamaContainerImageTags
public const string Registry = "docker.io";
public const string Image = "ollama/ollama";
public const string Tag = "0.3.11";

public const string OpenWebUIRegistry = "ghcr.io";
public const string OpenWebUIImage = "open-webui/open-webui";
public const string OpenWebUITag = "0.3.30";
}
12 changes: 6 additions & 6 deletions src/Aspire.CommunityToolkit.Hosting.Ollama/OllamaResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
/// <param name="modelName">The LLM to download on initial startup.</param>
public class OllamaResource(string name) : ContainerResource(name), IResourceWithConnectionString
{
internal const string OllamaEndpointName = "ollama";
internal const string OllamaEndpointName = "http";

private readonly List<string> _models = [];

private string? _defaultModel = null;

private EndpointReference? _endpointReference;
private EndpointReference? _primaryEndpointReference;

/// <summary>
/// Adds a model to the list of models to download on initial startup.
Expand All @@ -31,14 +31,14 @@ public class OllamaResource(string name) : ContainerResource(name), IResourceWit
/// <summary>
/// Gets the endpoint for the Ollama server.
/// </summary>
public EndpointReference Endpoint => _endpointReference ??= new(this, OllamaEndpointName);
public EndpointReference PrimaryEndpoint => _primaryEndpointReference ??= new(this, OllamaEndpointName);

/// <summary>
/// Gets the connection string expression for the Ollama server.
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create(
$"http://{Endpoint.Property(EndpointProperty.Host)}:{Endpoint.Property(EndpointProperty.Port)}"
$"{PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"
);

/// <summary>
Expand All @@ -47,7 +47,7 @@ public class OllamaResource(string name) : ContainerResource(name), IResourceWit
/// <param name="modelName">The name of the model</param>
public void AddModel(string modelName)
{
ArgumentNullException.ThrowIfNullOrEmpty(modelName, nameof(modelName));
ArgumentException.ThrowIfNullOrEmpty(modelName, nameof(modelName));
if (!_models.Contains(modelName))
{
_models.Add(modelName);
Expand All @@ -63,7 +63,7 @@ public void AddModel(string modelName)
/// </remarks>
public void SetDefaultModel(string modelName)
{
ArgumentNullException.ThrowIfNullOrEmpty(modelName, nameof(modelName));
ArgumentException.ThrowIfNullOrEmpty(modelName, nameof(modelName));
_defaultModel = modelName;

if (!_models.Contains(modelName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,61 @@ public static IResourceBuilder<OllamaResource> WithDefaultModel(this IResourceBu
builder.Resource.SetDefaultModel(modelName);
return builder;
}


/// <summary>
/// Adds an administration web UI Ollama to the application model using Attu. This version the package defaults to the main tag of the Open WebUI container image
/// </summary>
/// <example>
/// Use in application host with an Ollama resource
/// <code lang="csharp">
/// var builder = DistributedApplication.CreateBuilder(args);
///
/// var ollama = builder.AddOllama("ollama")
/// .WithOpenWebUI();
/// var api = builder.AddProject&lt;Projects.Api&gt;("api")
/// .WithReference(ollama);
///
/// builder.Build().Run();
/// </code>
/// </example>
/// <param name="builder">The Ollama resource builder.</param>
/// <param name="configureContainer">Configuration callback for Open WebUI container resource.</param>
/// <param name="containerName">The name of the container (Optional).</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>See https://openwebui.com for more information about Open WebUI</remarks>
public static IResourceBuilder<T> WithOpenWebUI<T>(this IResourceBuilder<T> builder, Action<IResourceBuilder<OpenWebUIResource>>? configureContainer = null, string? containerName = null) where T : OllamaResource
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));

if (builder.ApplicationBuilder.Resources.OfType<OpenWebUIResource>().SingleOrDefault() is { } existingOpenWebUIResource)
{
var builderForExistingResource = builder.ApplicationBuilder.CreateResourceBuilder(existingOpenWebUIResource);
configureContainer?.Invoke(builderForExistingResource);
return builder;
}

containerName ??= $"{builder.Resource.Name}-openwebui";

var openWebUI = new OpenWebUIResource(containerName);
var resourceBuilder = builder.ApplicationBuilder.AddResource(openWebUI)
.WithImage(OllamaContainerImageTags.OpenWebUIImage, OllamaContainerImageTags.OpenWebUITag)
.WithImageRegistry(OllamaContainerImageTags.OpenWebUIRegistry)
.WithHttpEndpoint(targetPort: 8080, name: "http")
.WithVolume("open-webui", "/app/backend/data")
.WithEnvironment(context => ConfigureOpenWebUIContainer(context, builder.Resource))
.ExcludeFromManifest();

configureContainer?.Invoke(resourceBuilder);

return builder;
}

private static void ConfigureOpenWebUIContainer(EnvironmentCallbackContext context, OllamaResource resource)
{
context.EnvironmentVariables.Add("ENABLE_SIGNUP", "false");
context.EnvironmentVariables.Add("ENABLE_COMMUNITY_SHARING", "false"); // by default don't enable sharing
context.EnvironmentVariables.Add("WEBUI_AUTH", "false"); // https://docs.openwebui.com/#quick-start-with-docker--recommended
context.EnvironmentVariables.Add("OLLAMA_BASE_URL", $"{resource.PrimaryEndpoint.Scheme}://{resource.PrimaryEndpoint.ContainerHost}:{resource.PrimaryEndpoint.Port}");
}
}
23 changes: 23 additions & 0 deletions src/Aspire.CommunityToolkit.Hosting.Ollama/OpenWebUIResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// A resource that represents an Open WebUI resource
/// </summary>
public class OpenWebUIResource(string name) : ContainerResource(name), IResourceWithConnectionString
{
internal const string PrimaryEndpointName = "http";

private EndpointReference? _primaryEndpoint;

/// <summary>
/// Gets the http endpoint for the Open WebUI resource.
/// </summary>
public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);

/// <summary>
/// Gets the connection string expression for the Open WebUI endpoint.
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create(
$"{PrimaryEndpoint.Property(EndpointProperty.Url)}");
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Aspire.CommunityToolkit.Hosting.Ollama.Tests;

public class ResourceCreationTests
public class AddOllamaTests
{
[Fact]
public void VerifyDefaultModel()
Expand Down Expand Up @@ -145,4 +145,20 @@ public void DefaultModelCannotBeOmitted()
Assert.Throws<ArgumentException>(() => ollama.WithDefaultModel(" "));
Assert.Throws<ArgumentNullException>(() => ollama.WithDefaultModel(null!));
}

[Fact]
public void OpenWebUIConfigured()
{
var builder = DistributedApplication.CreateBuilder();
_ = builder.AddOllama("ollama", port: null).WithOpenWebUI();

using var app = builder.Build();

var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var resource = Assert.Single(appModel.Resources.OfType<OpenWebUIResource>());

Assert.Equal("ollama-openwebui", resource.Name);
Assert.Equal("http", resource.PrimaryEndpoint.EndpointName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ namespace Aspire.CommunityToolkit.Hosting.Ollama.Tests;

public class AppHostTests(AspireIntegrationTestFixture<Projects.Aspire_CommunityToolkit_Hosting_Ollama_AppHost> fixture) : IClassFixture<AspireIntegrationTestFixture<Projects.Aspire_CommunityToolkit_Hosting_Ollama_AppHost>>
{
[ConditionalFact]
[ConditionalTheory]
[OSSkipCondition(OperatingSystems.Windows)]
public async Task ResourceStartsAndRespondsOk()
[InlineData("ollama")]
[InlineData("ollama-openwebui")]
public async Task ResourceStartsAndRespondsOk(string resourceName)
{
await fixture.ResourceNotificationService.WaitForResourceAsync("ollama", KnownResourceStates.Running).WaitAsync(TimeSpan.FromMinutes(5));
var httpClient = fixture.CreateHttpClient("ollama", "ollama");
await fixture.ResourceNotificationService.WaitForResourceAsync(resourceName, KnownResourceStates.Running).WaitAsync(TimeSpan.FromMinutes(5));
var httpClient = fixture.CreateHttpClient(resourceName);

var response = await httpClient.GetAsync("/");

Expand Down
18 changes: 17 additions & 1 deletion tests/Aspire.CommunityToolkit.Testing/AspireIntegrationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
namespace Aspire.CommunityToolkit.Testing;

public class AspireIntegrationTestFixture<TEntryPoint>() : DistributedApplicationFactory(typeof(TEntryPoint), []), IAsyncLifetime where TEntryPoint : class
Expand Down Expand Up @@ -33,5 +34,20 @@ protected override void OnBuilderCreated(DistributedApplicationBuilder applicati

public async Task InitializeAsync() => await StartAsync();

async Task IAsyncLifetime.DisposeAsync() => await DisposeAsync();
async Task IAsyncLifetime.DisposeAsync()
{
try
{
await DisposeAsync();
}
catch (Exception)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")))
{
// GitHub Actions Windows runners don't support Linux Docker containers, which can result in a bunch of false errors, even if we try to skip the test run, so we only really want to throw
// if we're on a non-Windows runner or if we're on a Windows runner but not in a CI environment
throw;
}
}
}
}

0 comments on commit 63547b3

Please sign in to comment.