From c97c3c5fbcc65ff01133d1aebb9998bbb1db280b Mon Sep 17 00:00:00 2001 From: Oleksii Date: Thu, 25 Sep 2025 08:55:16 +0300 Subject: [PATCH 1/5] Implemented a demo for Ollamasharp library --- ollamasharp/.gitignore | 5 ++ ollamasharp/Apps/OllamaSharpSample.cs | 96 +++++++++++++++++++++++++++ ollamasharp/Dockerfile | 34 ++++++++++ ollamasharp/GlobalUsings.cs | 26 ++++++++ ollamasharp/OllamaSharpDemo.csproj | 24 +++++++ ollamasharp/OllamaSharpDemo.sln | 24 +++++++ ollamasharp/Program.cs | 11 +++ ollamasharp/README.md | 17 +++++ 8 files changed, 237 insertions(+) create mode 100644 ollamasharp/.gitignore create mode 100644 ollamasharp/Apps/OllamaSharpSample.cs create mode 100644 ollamasharp/Dockerfile create mode 100644 ollamasharp/GlobalUsings.cs create mode 100644 ollamasharp/OllamaSharpDemo.csproj create mode 100644 ollamasharp/OllamaSharpDemo.sln create mode 100644 ollamasharp/Program.cs create mode 100644 ollamasharp/README.md diff --git a/ollamasharp/.gitignore b/ollamasharp/.gitignore new file mode 100644 index 00000000..2cb90536 --- /dev/null +++ b/ollamasharp/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +*.sln.iml +.idea/**/* +**/.idea/* \ No newline at end of file diff --git a/ollamasharp/Apps/OllamaSharpSample.cs b/ollamasharp/Apps/OllamaSharpSample.cs new file mode 100644 index 00000000..9d88ec24 --- /dev/null +++ b/ollamasharp/Apps/OllamaSharpSample.cs @@ -0,0 +1,96 @@ +using System.Text; +using OllamaSharp; +using Chat = Ivy.Chat; +using ChatMessage = Ivy.ChatMessage; + +namespace OllamaSharpDemo.Apps; + +[App(icon: Icons.TextQuote, title: "OllamaSharp")] +public class OllamaSharpSample : ViewBase +{ + private const string Url = "http://localhost:11434"; + private IState> messages; + private IState> models; + private IState selectedModel; + private OllamaApiClient ollamaApiClient; + + public override object? Build() + { + messages = UseState(ImmutableArray.Create()); + + models = UseState(ImmutableArray.Create); + selectedModel = UseState(); + + if (selectedModel.Value == null) + return Layout.Center() + | (new Card( + Layout.Vertical().Gap(6).Padding(2) + | new Button("Refresh Models", onClick: _ => OnRefreshClicked()) + | Layout.Vertical(models.Value) + ) + ) + .Width(Size.Units(120).Max(500)); + + return Layout.Vertical().Padding(0, 10, 0, 10) + | Text.H4($"Chatting with model: {selectedModel.Value?.ModelName}") + | new Button("Back to Models", onClick: _ => + { + ClearModelSelection(); + }) + | new Chat(messages.Value.ToArray(), OnSendMessage).Width(Size.Full().Max(200)); + } + + private async ValueTask OnSendMessage(Event @event) + { + messages.Set(messages.Value.Add(new ChatMessage(ChatSender.User, @event.Value))); + ollamaApiClient.SelectedModel = selectedModel.Value.ModelName; + var chat = new OllamaSharp.Chat(ollamaApiClient, @event.Value); + var builder = new StringBuilder(); + await foreach (var answerToken in chat.SendAsync(@event.Value)) + { + builder.Append(answerToken); + } + + messages.Set(messages.Value.Add(new ChatMessage(ChatSender.Assistant, builder.ToString()))); + } + + private async ValueTask OnRefreshClicked() + { + ollamaApiClient?.Dispose(); + ollamaApiClient = new OllamaApiClient(Url); + var connected = await ollamaApiClient.IsRunningAsync(); + if (!connected) + { + var client = UseService(); + client.Toast($"Ollama API is not running at {Url}", "Connection Error"); + return; + } + + var ollamaModels = await ollamaApiClient.ListLocalModelsAsync(); + models.Set(ollamaModels.Select(m => new ModelListItem(s => { selectedModel.Set(s); }, m.Name)) + .ToImmutableArray()); + ClearModelSelection(); + } + + private void ClearModelSelection() + { + selectedModel.Set((ModelListItem?)null); + messages.Set([]); + } +} + +public class ModelListItem(Action modelSelect, string modelName) : ViewBase +{ + public string ModelName { get; } = modelName; + + public override object? Build() + { + return Layout.Vertical( + Layout.Horizontal( + new Button(ModelName, _ => { modelSelect(this); }).Icon(Icons.NotebookText) + .Variant(ButtonVariant.Outline) + ).Align(Align.Left).Width(Size.Units(120).Max(500)), + new Separator() + ); + } +} \ No newline at end of file diff --git a/ollamasharp/Dockerfile b/ollamasharp/Dockerfile new file mode 100644 index 00000000..7a14dfe9 --- /dev/null +++ b/ollamasharp/Dockerfile @@ -0,0 +1,34 @@ +# Base runtime image +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +WORKDIR /app +EXPOSE 80 + +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src + +# Copy and restore +COPY ["OllamaSharpDemo.csproj", "./"] +RUN dotnet restore "OllamaSharpDemo.csproj" + +# Copy everything and build +COPY . . +RUN dotnet build "OllamaSharpDemo.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# Publish stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "OllamaSharpDemo.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true + +# Final runtime image +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . + +# Set environment variables +ENV PORT=80 +ENV ASPNETCORE_URLS="http://+:80" + +# Run the executable +ENTRYPOINT ["dotnet","./OllamaSharpDemo.dll"] \ No newline at end of file diff --git a/ollamasharp/GlobalUsings.cs b/ollamasharp/GlobalUsings.cs new file mode 100644 index 00000000..8889495b --- /dev/null +++ b/ollamasharp/GlobalUsings.cs @@ -0,0 +1,26 @@ +global using Ivy; +global using Ivy.Apps; +global using Ivy.Auth; +global using Ivy.Chrome; +global using Ivy.Client; +global using Ivy.Core; +global using Ivy.Core.Hooks; +global using Ivy.Helpers; +global using Ivy.Hooks; +global using Ivy.Shared; +global using Ivy.Views; +global using Ivy.Views.Alerts; +global using Ivy.Views.Blades; +global using Ivy.Views.Builders; +global using Ivy.Views.Charts; +global using Ivy.Views.Dashboards; +global using Ivy.Views.Forms; +global using Ivy.Views.Tables; +global using Ivy.Widgets.Inputs; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using System.Collections.Immutable; +global using System.ComponentModel.DataAnnotations; +global using System.Globalization; +global using System.Reactive.Linq; \ No newline at end of file diff --git a/ollamasharp/OllamaSharpDemo.csproj b/ollamasharp/OllamaSharpDemo.csproj new file mode 100644 index 00000000..7e1b81b1 --- /dev/null +++ b/ollamasharp/OllamaSharpDemo.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + CS8618;CS8603;CS8602;CS8604;CS9113 + OllamaSharpDemo + + + + + + + + + + + + + + + diff --git a/ollamasharp/OllamaSharpDemo.sln b/ollamasharp/OllamaSharpDemo.sln new file mode 100644 index 00000000..4417834b --- /dev/null +++ b/ollamasharp/OllamaSharpDemo.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hello", "OllamaSharpDemo.csproj", "{039638F3-A6D7-FB67-E733-27BD896EFDC4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {039638F3-A6D7-FB67-E733-27BD896EFDC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {039638F3-A6D7-FB67-E733-27BD896EFDC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {039638F3-A6D7-FB67-E733-27BD896EFDC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {039638F3-A6D7-FB67-E733-27BD896EFDC4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5F74B1AD-6C2E-4FDA-9DAF-9F7D8053687B} + EndGlobalSection +EndGlobal diff --git a/ollamasharp/Program.cs b/ollamasharp/Program.cs new file mode 100644 index 00000000..ff8aa007 --- /dev/null +++ b/ollamasharp/Program.cs @@ -0,0 +1,11 @@ +using OllamaSharpDemo.Apps; +CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US"); +var server = new Server(); +#if DEBUG +server.UseHotReload(); +#endif +server.AddAppsFromAssembly(); +server.AddConnectionsFromAssembly(); +var chromeSettings = new ChromeSettings().DefaultApp().UseTabs(preventDuplicates: true); +server.UseChrome(chromeSettings); +await server.RunAsync(); diff --git a/ollamasharp/README.md b/ollamasharp/README.md new file mode 100644 index 00000000..37a05000 --- /dev/null +++ b/ollamasharp/README.md @@ -0,0 +1,17 @@ +# Hello + +Web application created using [Ivy](https://github.com/Ivy-Interactive/Ivy). + +Ivy is a web framework for building interactive web applications using C# and .NET. + +## Run + +``` +dotnet watch +``` + +## Deploy + +``` +ivy deploy +``` \ No newline at end of file From ac7763e60ce16ba55d55eb4739324219da158016 Mon Sep 17 00:00:00 2001 From: Oleksii Date: Thu, 25 Sep 2025 08:59:14 +0300 Subject: [PATCH 2/5] removed Dockerfile --- ollamasharp/Dockerfile | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 ollamasharp/Dockerfile diff --git a/ollamasharp/Dockerfile b/ollamasharp/Dockerfile deleted file mode 100644 index 7a14dfe9..00000000 --- a/ollamasharp/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -# Base runtime image -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base -WORKDIR /app -EXPOSE 80 - -# Build stage -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build -ARG BUILD_CONFIGURATION=Release -WORKDIR /src - -# Copy and restore -COPY ["OllamaSharpDemo.csproj", "./"] -RUN dotnet restore "OllamaSharpDemo.csproj" - -# Copy everything and build -COPY . . -RUN dotnet build "OllamaSharpDemo.csproj" -c $BUILD_CONFIGURATION -o /app/build - -# Publish stage -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "OllamaSharpDemo.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true - -# Final runtime image -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . - -# Set environment variables -ENV PORT=80 -ENV ASPNETCORE_URLS="http://+:80" - -# Run the executable -ENTRYPOINT ["dotnet","./OllamaSharpDemo.dll"] \ No newline at end of file From 44ecfdd14fa05ce8bc9b42dae7146d01a1a701ff Mon Sep 17 00:00:00 2001 From: Oleksii Date: Thu, 25 Sep 2025 10:10:35 +0300 Subject: [PATCH 3/5] Readme update --- .../{OllamaSharpSample.cs => OllamaSharp.cs} | 2 +- ollamasharp/Program.cs | 2 +- ollamasharp/README.md | 119 +++++++++++++++++- 3 files changed, 120 insertions(+), 3 deletions(-) rename ollamasharp/Apps/{OllamaSharpSample.cs => OllamaSharp.cs} (98%) diff --git a/ollamasharp/Apps/OllamaSharpSample.cs b/ollamasharp/Apps/OllamaSharp.cs similarity index 98% rename from ollamasharp/Apps/OllamaSharpSample.cs rename to ollamasharp/Apps/OllamaSharp.cs index 9d88ec24..8a5b2b39 100644 --- a/ollamasharp/Apps/OllamaSharpSample.cs +++ b/ollamasharp/Apps/OllamaSharp.cs @@ -6,7 +6,7 @@ namespace OllamaSharpDemo.Apps; [App(icon: Icons.TextQuote, title: "OllamaSharp")] -public class OllamaSharpSample : ViewBase +public class OllamaSharp : ViewBase { private const string Url = "http://localhost:11434"; private IState> messages; diff --git a/ollamasharp/Program.cs b/ollamasharp/Program.cs index ff8aa007..19f1e4b3 100644 --- a/ollamasharp/Program.cs +++ b/ollamasharp/Program.cs @@ -6,6 +6,6 @@ #endif server.AddAppsFromAssembly(); server.AddConnectionsFromAssembly(); -var chromeSettings = new ChromeSettings().DefaultApp().UseTabs(preventDuplicates: true); +var chromeSettings = new ChromeSettings().DefaultApp().UseTabs(preventDuplicates: true); server.UseChrome(chromeSettings); await server.RunAsync(); diff --git a/ollamasharp/README.md b/ollamasharp/README.md index 37a05000..eb91183d 100644 --- a/ollamasharp/README.md +++ b/ollamasharp/README.md @@ -14,4 +14,121 @@ dotnet watch ``` ivy deploy -``` \ No newline at end of file +``` + +# OllamaSharp Demo + +This project demonstrates how to integrate [Ollama](https://ollama.com/) with an Ivy application using the [OllamaSharp](https://github.com/awaescher/OllamaSharp) library. + +## What is OllamaSharp? + +The `OllamaSharp` is an Ivy application that provides a chat interface for interacting with local Large Language Models (LLMs) through Ollama. The application features: + +- **Model Selection**: Browse and select from locally available Ollama models +- **Interactive Chat**: Real-time chat interface with streaming responses +- **Model Management**: Easy switching between different models +- **Connection Status**: Automatic detection of Ollama service availability + +The sample demonstrates how to: +- Connect to a local Ollama API instance +- List available models +- Send chat messages and receive streaming responses +- Handle connection errors and service availability + +## Prerequisites + +### 1. Install Ollama + +First, you need to install Ollama on your local machine: + +#### Windows +1. Download Ollama from [https://ollama.com/download](https://ollama.com/download) +2. Run the installer +3. Ollama will automatically start as a service + +#### macOS +```bash +# Using Homebrew +brew install ollama + +# Or download from https://ollama.com/download +``` + +#### Linux +```bash +curl -fsSL https://ollama.com/install.sh | sh +``` + +### 2. Download Models + +After installing Ollama, you need to download at least one model to use with the application: + +```bash +# Download a popular model (this may take a while depending on model size) +ollama pull llama2 + +# Or try a smaller model for testing +ollama pull phi3 + +# List other available models +ollama list +``` + +### 3. Start Ollama Service + +Ensure Ollama is running locally: + +```bash +# Start Ollama (if not running as a service) +ollama serve +``` + +By default, Ollama runs on `http://localhost:11434` + +## Running the Application + +1. **Clone and navigate to the project**: + ```bash + cd ollamasharp + ``` + +2. **Restore dependencies**: + ```bash + dotnet restore + ``` + +3. **Run the application**: + ```bash + dotnet run + ``` + +4. **Use the application**: + - Click "Refresh Models" to load available Ollama models + - Select a model from the list + - Start chatting with the selected model + - Use "Back to Models" to switch to a different model + +## Troubleshooting + +### "Ollama API is not running" Error + +If you see this error: +1. Ensure Ollama is installed and running: `ollama serve` +2. Check if Ollama is accessible: `curl http://localhost:11434` +3. Verify you have at least one model downloaded: `ollama list` + +### No Models Available + +If no models appear in the list: +1. Download a model: `ollama pull llama2` +2. Verify models are installed: `ollama list` +3. Restart the application and click "Refresh Models" + +## Dependencies + +- **OllamaSharp**: .NET client library for Ollama API +- **Ivy Framework**: UI framework for building the chat interface + +## Configuration + +The application connects to Ollama on `http://localhost:11434` by default. You can modify the `Url` constant in `OllamaSharp.cs` to connect to a different Ollama instance if needed. From 01d87076af6e4d72636079490890c6acf133ada4 Mon Sep 17 00:00:00 2001 From: Oleksii Date: Thu, 25 Sep 2025 17:11:19 +0300 Subject: [PATCH 4/5] Fix the build --- ollamasharp/Apps/OllamaSharp.cs | 2 +- ollamasharp/Program.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ollamasharp/Apps/OllamaSharp.cs b/ollamasharp/Apps/OllamaSharp.cs index 8a5b2b39..9d88ec24 100644 --- a/ollamasharp/Apps/OllamaSharp.cs +++ b/ollamasharp/Apps/OllamaSharp.cs @@ -6,7 +6,7 @@ namespace OllamaSharpDemo.Apps; [App(icon: Icons.TextQuote, title: "OllamaSharp")] -public class OllamaSharp : ViewBase +public class OllamaSharpSample : ViewBase { private const string Url = "http://localhost:11434"; private IState> messages; diff --git a/ollamasharp/Program.cs b/ollamasharp/Program.cs index 19f1e4b3..ff8aa007 100644 --- a/ollamasharp/Program.cs +++ b/ollamasharp/Program.cs @@ -6,6 +6,6 @@ #endif server.AddAppsFromAssembly(); server.AddConnectionsFromAssembly(); -var chromeSettings = new ChromeSettings().DefaultApp().UseTabs(preventDuplicates: true); +var chromeSettings = new ChromeSettings().DefaultApp().UseTabs(preventDuplicates: true); server.UseChrome(chromeSettings); await server.RunAsync(); From 65461ced75084eaf4f6f79162d2eb8b8605c54b3 Mon Sep 17 00:00:00 2001 From: Oleksii Date: Thu, 25 Sep 2025 17:17:18 +0300 Subject: [PATCH 5/5] Code review comment fixes --- ollamasharp/Apps/OllamaSharp.cs | 56 ++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/ollamasharp/Apps/OllamaSharp.cs b/ollamasharp/Apps/OllamaSharp.cs index 9d88ec24..57f45eab 100644 --- a/ollamasharp/Apps/OllamaSharp.cs +++ b/ollamasharp/Apps/OllamaSharp.cs @@ -9,56 +9,68 @@ namespace OllamaSharpDemo.Apps; public class OllamaSharpSample : ViewBase { private const string Url = "http://localhost:11434"; - private IState> messages; - private IState> models; - private IState selectedModel; - private OllamaApiClient ollamaApiClient; + private IState> _messages; + private IState> _models; + private IState _selectedModel; + private OllamaApiClient? _ollamaApiClient; public override object? Build() { - messages = UseState(ImmutableArray.Create()); + _messages = UseState(ImmutableArray.Create()); - models = UseState(ImmutableArray.Create); - selectedModel = UseState(); + _models = UseState(ImmutableArray.Create); + _selectedModel = UseState(); - if (selectedModel.Value == null) + if (_selectedModel.Value == null) return Layout.Center() | (new Card( Layout.Vertical().Gap(6).Padding(2) | new Button("Refresh Models", onClick: _ => OnRefreshClicked()) - | Layout.Vertical(models.Value) + | Layout.Vertical(_models.Value) ) ) .Width(Size.Units(120).Max(500)); return Layout.Vertical().Padding(0, 10, 0, 10) - | Text.H4($"Chatting with model: {selectedModel.Value?.ModelName}") + | Text.H4($"Chatting with model: {_selectedModel.Value?.ModelName}") | new Button("Back to Models", onClick: _ => { ClearModelSelection(); }) - | new Chat(messages.Value.ToArray(), OnSendMessage).Width(Size.Full().Max(200)); + | new Chat(_messages.Value.ToArray(), OnSendMessage).Width(Size.Full().Max(200)); } private async ValueTask OnSendMessage(Event @event) { - messages.Set(messages.Value.Add(new ChatMessage(ChatSender.User, @event.Value))); - ollamaApiClient.SelectedModel = selectedModel.Value.ModelName; - var chat = new OllamaSharp.Chat(ollamaApiClient, @event.Value); + if (_ollamaApiClient == null) + { + var clientWarn = UseService(); + clientWarn.Toast("Click 'Refresh Models' first to initialize the API client.", "Not Ready"); + return; + } + if (_selectedModel.Value == null) + { + var clientWarn = UseService(); + clientWarn.Toast("Select a model before chatting.", "Model Required"); + return; + } + _messages.Set(_messages.Value.Add(new ChatMessage(ChatSender.User, @event.Value))); + _ollamaApiClient.SelectedModel = _selectedModel.Value.ModelName; + var chat = new OllamaSharp.Chat(_ollamaApiClient, @event.Value); var builder = new StringBuilder(); await foreach (var answerToken in chat.SendAsync(@event.Value)) { builder.Append(answerToken); } - messages.Set(messages.Value.Add(new ChatMessage(ChatSender.Assistant, builder.ToString()))); + _messages.Set(_messages.Value.Add(new ChatMessage(ChatSender.Assistant, builder.ToString()))); } private async ValueTask OnRefreshClicked() { - ollamaApiClient?.Dispose(); - ollamaApiClient = new OllamaApiClient(Url); - var connected = await ollamaApiClient.IsRunningAsync(); + _ollamaApiClient?.Dispose(); + _ollamaApiClient = new OllamaApiClient(Url); + var connected = await _ollamaApiClient.IsRunningAsync(); if (!connected) { var client = UseService(); @@ -66,16 +78,16 @@ private async ValueTask OnRefreshClicked() return; } - var ollamaModels = await ollamaApiClient.ListLocalModelsAsync(); - models.Set(ollamaModels.Select(m => new ModelListItem(s => { selectedModel.Set(s); }, m.Name)) + var ollamaModels = await _ollamaApiClient.ListLocalModelsAsync(); + _models.Set(ollamaModels.Select(m => new ModelListItem(s => { _selectedModel.Set(s); }, m.Name)) .ToImmutableArray()); ClearModelSelection(); } private void ClearModelSelection() { - selectedModel.Set((ModelListItem?)null); - messages.Set([]); + _selectedModel.Set((ModelListItem?)null); + _messages.Set([]); } }