From a5d02a05da1133dfe5888d7b80decc9f968f97df Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Sat, 14 Jun 2025 14:39:41 +0000
Subject: [PATCH 1/4] Add extensions + UT
---
...aKernelBuilderExtensionsChatClientTests.cs | 123 ++++++++
...viceCollectionExtensionsChatClientTests.cs | 150 ++++++++++
.../Services/OllamaChatClientTests.cs | 273 ++++++++++++++++++
.../OllamaKernelBuilderExtensions.cs | 65 +++++
...ollectionExtensions.DependencyInjection.cs | 115 +++++++-
.../OllamaChatClientIntegrationTests.cs | 232 +++++++++++++++
6 files changed, 957 insertions(+), 1 deletion(-)
create mode 100644 dotnet/src/Connectors/Connectors.Ollama.UnitTests/Extensions/OllamaKernelBuilderExtensionsChatClientTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.Ollama.UnitTests/Extensions/OllamaServiceCollectionExtensionsChatClientTests.cs
create mode 100644 dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaChatClientTests.cs
create mode 100644 dotnet/src/IntegrationTests/Connectors/Ollama/OllamaChatClientIntegrationTests.cs
diff --git a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Extensions/OllamaKernelBuilderExtensionsChatClientTests.cs b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Extensions/OllamaKernelBuilderExtensionsChatClientTests.cs
new file mode 100644
index 000000000000..6065b8d73510
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Extensions/OllamaKernelBuilderExtensionsChatClientTests.cs
@@ -0,0 +1,123 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Net.Http;
+using Microsoft.Extensions.AI;
+using Microsoft.SemanticKernel;
+using OllamaSharp;
+using Xunit;
+
+namespace SemanticKernel.Connectors.Ollama.UnitTests.Extensions;
+
+///
+/// Unit tests of for IChatClient.
+///
+public class OllamaKernelBuilderExtensionsChatClientTests
+{
+ [Fact]
+ public void AddOllamaChatClientNullArgsThrow()
+ {
+ // Arrange
+ IKernelBuilder builder = null!;
+ string modelId = "llama3.2";
+ var endpoint = new Uri("http://localhost:11434");
+ string serviceId = "test_service_id";
+
+ // Act & Assert
+ var exception = Assert.Throws(() => builder.AddOllamaChatClient(modelId, endpoint, serviceId));
+ Assert.Equal("builder", exception.ParamName);
+
+ using var httpClient = new HttpClient();
+ exception = Assert.Throws(() => builder.AddOllamaChatClient(modelId, httpClient, serviceId));
+ Assert.Equal("builder", exception.ParamName);
+
+ exception = Assert.Throws(() => builder.AddOllamaChatClient(null, serviceId));
+ Assert.Equal("builder", exception.ParamName);
+ }
+
+ [Fact]
+ public void AddOllamaChatClientWithEndpointValidParametersRegistersService()
+ {
+ // Arrange
+ var builder = Kernel.CreateBuilder();
+ string modelId = "llama3.2";
+ var endpoint = new Uri("http://localhost:11434");
+ string serviceId = "test_service_id";
+
+ // Act
+ builder.AddOllamaChatClient(modelId, endpoint, serviceId);
+
+ // Assert
+ var kernel = builder.Build();
+ Assert.NotNull(kernel.GetRequiredService());
+ Assert.NotNull(kernel.GetRequiredService(serviceId));
+ }
+
+ [Fact]
+ public void AddOllamaChatClientWithHttpClientValidParametersRegistersService()
+ {
+ // Arrange
+ var builder = Kernel.CreateBuilder();
+ string modelId = "llama3.2";
+ using var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:11434") };
+ string serviceId = "test_service_id";
+
+ // Act
+ builder.AddOllamaChatClient(modelId, httpClient, serviceId);
+
+ // Assert
+ var kernel = builder.Build();
+ Assert.NotNull(kernel.GetRequiredService());
+ Assert.NotNull(kernel.GetRequiredService(serviceId));
+ }
+
+ [Fact]
+ public void AddOllamaChatClientWithOllamaClientValidParametersRegistersService()
+ {
+ // Arrange
+ var builder = Kernel.CreateBuilder();
+ using var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:11434") };
+ using var ollamaClient = new OllamaApiClient(httpClient, "llama3.2");
+ string serviceId = "test_service_id";
+
+ // Act
+ builder.AddOllamaChatClient(ollamaClient, serviceId);
+
+ // Assert
+ var kernel = builder.Build();
+ Assert.NotNull(kernel.GetRequiredService());
+ Assert.NotNull(kernel.GetRequiredService(serviceId));
+ }
+
+ [Fact]
+ public void AddOllamaChatClientWithoutServiceIdRegistersDefaultService()
+ {
+ // Arrange
+ var builder = Kernel.CreateBuilder();
+ string modelId = "llama3.2";
+ var endpoint = new Uri("http://localhost:11434");
+
+ // Act
+ builder.AddOllamaChatClient(modelId, endpoint);
+
+ // Assert
+ var kernel = builder.Build();
+ Assert.NotNull(kernel.GetRequiredService());
+ }
+
+ [Fact]
+ public void AddOllamaChatClientWithHttpClientWithoutServiceIdRegistersDefaultService()
+ {
+ // Arrange
+ var builder = Kernel.CreateBuilder();
+ string modelId = "llama3.2";
+ using var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:11434") };
+
+ // Act
+ builder.AddOllamaChatClient(modelId, httpClient);
+
+ // Assert
+ var kernel = builder.Build();
+ Assert.NotNull(kernel.GetRequiredService());
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Extensions/OllamaServiceCollectionExtensionsChatClientTests.cs b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Extensions/OllamaServiceCollectionExtensionsChatClientTests.cs
new file mode 100644
index 000000000000..af4844e4a70f
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Extensions/OllamaServiceCollectionExtensionsChatClientTests.cs
@@ -0,0 +1,150 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Net.Http;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.SemanticKernel;
+using OllamaSharp;
+using Xunit;
+
+namespace SemanticKernel.Connectors.Ollama.UnitTests.Extensions;
+
+///
+/// Unit tests of for IChatClient.
+///
+public class OllamaServiceCollectionExtensionsChatClientTests
+{
+ [Fact]
+ public void AddOllamaChatClientNullArgsThrow()
+ {
+ // Arrange
+ IServiceCollection services = null!;
+ string modelId = "llama3.2";
+ var endpoint = new Uri("http://localhost:11434");
+ string serviceId = "test_service_id";
+
+ // Act & Assert
+ var exception = Assert.Throws(() => services.AddOllamaChatClient(modelId, endpoint, serviceId));
+ Assert.Equal("services", exception.ParamName);
+
+ using var httpClient = new HttpClient();
+ exception = Assert.Throws(() => services.AddOllamaChatClient(modelId, httpClient, serviceId));
+ Assert.Equal("services", exception.ParamName);
+
+ exception = Assert.Throws(() => services.AddOllamaChatClient(null, serviceId));
+ Assert.Equal("services", exception.ParamName);
+ }
+
+ [Fact]
+ public void AddOllamaChatClientWithEndpointValidParametersRegistersService()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ string modelId = "llama3.2";
+ var endpoint = new Uri("http://localhost:11434");
+ string serviceId = "test_service_id";
+
+ // Act
+ services.AddOllamaChatClient(modelId, endpoint, serviceId);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider();
+ var chatClient = serviceProvider.GetKeyedService(serviceId);
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ public void AddOllamaChatClientWithHttpClientValidParametersRegistersService()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ string modelId = "llama3.2";
+ using var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:11434") };
+ string serviceId = "test_service_id";
+
+ // Act
+ services.AddOllamaChatClient(modelId, httpClient, serviceId);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider();
+ var chatClient = serviceProvider.GetKeyedService(serviceId);
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ public void AddOllamaChatClientWithOllamaClientValidParametersRegistersService()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ using var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:11434") };
+ using var ollamaClient = new OllamaApiClient(httpClient, "llama3.2");
+ string serviceId = "test_service_id";
+
+ // Act
+ services.AddOllamaChatClient(ollamaClient, serviceId);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider();
+ var chatClient = serviceProvider.GetKeyedService(serviceId);
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ public void AddOllamaChatClientWorksWithKernel()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ string modelId = "llama3.2";
+ var endpoint = new Uri("http://localhost:11434");
+ string serviceId = "test_service_id";
+
+ // Act
+ services.AddOllamaChatClient(modelId, endpoint, serviceId);
+ services.AddKernel();
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider();
+ var kernel = serviceProvider.GetRequiredService();
+
+ var serviceFromCollection = serviceProvider.GetKeyedService(serviceId);
+ var serviceFromKernel = kernel.GetRequiredService(serviceId);
+
+ Assert.NotNull(serviceFromKernel);
+ Assert.Same(serviceFromCollection, serviceFromKernel);
+ }
+
+ [Fact]
+ public void AddOllamaChatClientWithoutServiceIdRegistersDefaultService()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ string modelId = "llama3.2";
+ var endpoint = new Uri("http://localhost:11434");
+
+ // Act
+ services.AddOllamaChatClient(modelId, endpoint);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider();
+ var chatClient = serviceProvider.GetService();
+ Assert.NotNull(chatClient);
+ }
+
+ [Fact]
+ public void AddOllamaChatClientWithHttpClientWithoutServiceIdRegistersDefaultService()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ string modelId = "llama3.2";
+ using var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:11434") };
+
+ // Act
+ services.AddOllamaChatClient(modelId, httpClient);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider();
+ var chatClient = serviceProvider.GetService();
+ Assert.NotNull(chatClient);
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaChatClientTests.cs b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaChatClientTests.cs
new file mode 100644
index 000000000000..1985637527d2
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Ollama.UnitTests/Services/OllamaChatClientTests.cs
@@ -0,0 +1,273 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using OllamaSharp;
+using OllamaSharp.Models.Chat;
+using Xunit;
+using ChatRole = Microsoft.Extensions.AI.ChatRole;
+
+namespace SemanticKernel.Connectors.Ollama.UnitTests.Services;
+
+public sealed class OllamaChatClientTests : IDisposable
+{
+ private readonly HttpClient _httpClient;
+ private readonly MultipleHttpMessageHandlerStub _multiMessageHandlerStub;
+ private readonly HttpResponseMessage _defaultResponseMessage;
+
+ public OllamaChatClientTests()
+ {
+ this._defaultResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
+ {
+ Content = new StreamContent(File.OpenRead("TestData/chat_completion_test_response.txt"))
+ };
+
+ this._multiMessageHandlerStub = new()
+ {
+ ResponsesToReturn = [this._defaultResponseMessage]
+ };
+ this._httpClient = new HttpClient(this._multiMessageHandlerStub, false) { BaseAddress = new Uri("http://localhost:11434") };
+ }
+
+ [Fact]
+ public async Task ShouldSendPromptToServiceAsync()
+ {
+ // Arrange
+ using var ollamaClient = new OllamaApiClient(this._httpClient, "fake-model");
+ var sut = (IChatClient)ollamaClient;
+ var messages = new List
+ {
+ new(ChatRole.User, "fake-text")
+ };
+
+ // Act
+ await sut.GetResponseAsync(messages);
+
+ // Assert
+ var requestPayload = JsonSerializer.Deserialize(this._multiMessageHandlerStub.RequestContents[0]);
+ Assert.NotNull(requestPayload);
+ Assert.Equal("fake-text", requestPayload.Messages!.First().Content);
+ }
+
+ [Fact]
+ public async Task ShouldHandleServiceResponseAsync()
+ {
+ // Arrange
+ using var ollamaClient = new OllamaApiClient(this._httpClient, "fake-model");
+ var sut = (IChatClient)ollamaClient;
+ var messages = new List
+ {
+ new(ChatRole.User, "fake-text")
+ };
+
+ // Act
+ var response = await sut.GetResponseAsync(messages);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal("This is test completion response", response.Text);
+ }
+
+ [Fact]
+ public async Task GetResponseShouldHaveModelIdAsync()
+ {
+ // Arrange
+ var expectedModel = "llama3.2";
+ using var ollamaClient = new OllamaApiClient(this._httpClient, expectedModel);
+ var sut = (IChatClient)ollamaClient;
+ var messages = new List
+ {
+ new(ChatRole.User, "fake-text")
+ };
+
+ // Act
+ var response = await sut.GetResponseAsync(messages);
+
+ // Assert
+ Assert.NotNull(response);
+
+ // Verify the request was sent with the correct model
+ var requestPayload = JsonSerializer.Deserialize(this._multiMessageHandlerStub.RequestContents[0]);
+ Assert.NotNull(requestPayload);
+ Assert.Equal(expectedModel, requestPayload.Model);
+ }
+
+ [Fact]
+ public async Task GetStreamingResponseShouldWorkAsync()
+ {
+ // Arrange
+ var expectedModel = "phi3";
+ using var streamResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
+ {
+ Content = new StreamContent(File.OpenRead("TestData/chat_completion_test_response_stream.txt"))
+ };
+ this._multiMessageHandlerStub.ResponsesToReturn = [streamResponse];
+
+ using var ollamaClient = new OllamaApiClient(this._httpClient, expectedModel);
+ var sut = (IChatClient)ollamaClient;
+ var messages = new List
+ {
+ new(ChatRole.User, "fake-text")
+ };
+
+ // Act
+ var responseUpdates = new List();
+ await foreach (var update in sut.GetStreamingResponseAsync(messages))
+ {
+ responseUpdates.Add(update);
+ }
+
+ // Assert
+ Assert.NotEmpty(responseUpdates);
+ var lastUpdate = responseUpdates.Last();
+ Assert.NotNull(lastUpdate);
+
+ // Verify the request was sent with the correct model
+ var requestPayload = JsonSerializer.Deserialize(this._multiMessageHandlerStub.RequestContents[0]);
+ Assert.NotNull(requestPayload);
+ Assert.Equal(expectedModel, requestPayload.Model);
+ }
+
+ [Fact]
+ public async Task GetResponseWithChatOptionsAsync()
+ {
+ // Arrange
+ var expectedModel = "fake-model";
+ using var ollamaClient = new OllamaApiClient(this._httpClient, expectedModel);
+ var sut = (IChatClient)ollamaClient;
+ var messages = new List
+ {
+ new(ChatRole.User, "fake-text")
+ };
+
+ var chatOptions = new ChatOptions
+ {
+ Temperature = 0.5f,
+ TopP = 0.9f,
+ MaxOutputTokens = 100,
+ StopSequences = ["stop me"]
+ };
+
+ // Act
+ await sut.GetResponseAsync(messages, chatOptions);
+
+ // Assert
+ var requestPayload = JsonSerializer.Deserialize(this._multiMessageHandlerStub.RequestContents[0]);
+ Assert.NotNull(requestPayload);
+ Assert.NotNull(requestPayload.Options);
+ Assert.Equal(chatOptions.Temperature, requestPayload.Options.Temperature);
+ Assert.Equal(chatOptions.TopP, requestPayload.Options.TopP);
+ Assert.Equal(chatOptions.StopSequences, requestPayload.Options.Stop);
+ }
+
+ [Fact]
+ public void GetServiceShouldReturnChatClientMetadata()
+ {
+ // Arrange
+ var expectedModel = "llama3.2";
+ using var ollamaClient = new OllamaApiClient(this._httpClient, expectedModel);
+ var sut = (IChatClient)ollamaClient;
+
+ // Act
+ var metadata = sut.GetService(typeof(ChatClientMetadata));
+
+ // Assert
+ Assert.NotNull(metadata);
+ Assert.IsType(metadata);
+ var chatMetadata = (ChatClientMetadata)metadata;
+ Assert.Equal(expectedModel, chatMetadata.DefaultModelId);
+ }
+
+ [Fact]
+ public async Task ShouldHandleCancellationTokenAsync()
+ {
+ // Arrange
+ using var ollamaClient = new OllamaApiClient(this._httpClient, "fake-model");
+ var sut = (IChatClient)ollamaClient;
+ var messages = new List
+ {
+ new(ChatRole.User, "fake-text")
+ };
+
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ await sut.GetResponseAsync(messages, cancellationToken: cts.Token));
+ }
+
+ [Fact]
+ public async Task ShouldWorkWithBuilderPatternAsync()
+ {
+ // Arrange
+ using var ollamaClient = new OllamaApiClient(this._httpClient, "fake-model");
+ IChatClient sut = ((IChatClient)ollamaClient).AsBuilder().Build();
+ var messages = new List
+ {
+ new(ChatRole.User, "fake-text")
+ };
+
+ // Act
+ var response = await sut.GetResponseAsync(messages);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal("This is test completion response", response.Text);
+ }
+
+ [Fact]
+ public void ShouldSupportDispose()
+ {
+ // Arrange
+ using var sut = new OllamaApiClient(this._httpClient, "fake-model");
+
+ // Act & Assert - Should not throw
+ ((IChatClient)sut).Dispose();
+ }
+
+ [Fact]
+ public async Task ShouldHandleMultipleMessagesAsync()
+ {
+ // Arrange
+ using var ollamaClient = new OllamaApiClient(this._httpClient, "fake-model");
+ IChatClient sut = ollamaClient;
+ var messages = new List
+ {
+ new(ChatRole.System, "You are a helpful assistant."),
+ new(ChatRole.User, "Hello"),
+ new(ChatRole.Assistant, "Hi there!"),
+ new(ChatRole.User, "How are you?")
+ };
+
+ // Act
+ var response = await sut.GetResponseAsync(messages);
+
+ // Assert
+ Assert.NotNull(response);
+
+ // Verify all messages were sent
+ var requestPayload = JsonSerializer.Deserialize(this._multiMessageHandlerStub.RequestContents[0]);
+ Assert.NotNull(requestPayload);
+ Assert.Equal(4, requestPayload.Messages!.Count());
+ var messagesList = requestPayload.Messages!.ToList();
+ Assert.Equal("system", messagesList[0].Role);
+ Assert.Equal("user", messagesList[1].Role);
+ Assert.Equal("assistant", messagesList[2].Role);
+ Assert.Equal("user", messagesList[3].Role);
+ }
+
+ public void Dispose()
+ {
+ this._httpClient?.Dispose();
+ this._defaultResponseMessage?.Dispose();
+ this._multiMessageHandlerStub?.Dispose();
+ }
+}
diff --git a/dotnet/src/Connectors/Connectors.Ollama/Extensions/OllamaKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.Ollama/Extensions/OllamaKernelBuilderExtensions.cs
index 7c0e95507dba..008337b033ab 100644
--- a/dotnet/src/Connectors/Connectors.Ollama/Extensions/OllamaKernelBuilderExtensions.cs
+++ b/dotnet/src/Connectors/Connectors.Ollama/Extensions/OllamaKernelBuilderExtensions.cs
@@ -164,6 +164,71 @@ public static IKernelBuilder AddOllamaChatCompletion(
#endregion
+ #region Chat Client
+
+ ///
+ /// Add Ollama Chat Client to the kernel builder.
+ ///
+ /// The kernel builder.
+ /// The model for text generation.
+ /// The endpoint to Ollama hosted service.
+ /// The optional service ID.
+ /// The updated kernel builder.
+ public static IKernelBuilder AddOllamaChatClient(
+ this IKernelBuilder builder,
+ string modelId,
+ Uri endpoint,
+ string? serviceId = null)
+ {
+ Verify.NotNull(builder);
+
+ builder.Services.AddOllamaChatClient(modelId, endpoint, serviceId);
+
+ return builder;
+ }
+
+ ///
+ /// Add Ollama Chat Client to the kernel builder.
+ ///
+ /// The kernel builder.
+ /// The model for text generation.
+ /// The optional custom HttpClient.
+ /// The optional service ID.
+ /// The updated kernel builder.
+ public static IKernelBuilder AddOllamaChatClient(
+ this IKernelBuilder builder,
+ string modelId,
+ HttpClient? httpClient = null,
+ string? serviceId = null)
+ {
+ Verify.NotNull(builder);
+
+ builder.Services.AddOllamaChatClient(modelId, httpClient, serviceId);
+
+ return builder;
+ }
+
+ ///
+ /// Add Ollama Chat Client to the kernel builder.
+ ///
+ /// The kernel builder.
+ /// The Ollama Sharp library client.
+ /// The optional service ID.
+ /// The updated kernel builder.
+ public static IKernelBuilder AddOllamaChatClient(
+ this IKernelBuilder builder,
+ OllamaApiClient? ollamaClient = null,
+ string? serviceId = null)
+ {
+ Verify.NotNull(builder);
+
+ builder.Services.AddOllamaChatClient(ollamaClient, serviceId);
+
+ return builder;
+ }
+
+ #endregion
+
#region Text Embeddings
///
diff --git a/dotnet/src/Connectors/Connectors.Ollama/Extensions/OllamaServiceCollectionExtensions.DependencyInjection.cs b/dotnet/src/Connectors/Connectors.Ollama/Extensions/OllamaServiceCollectionExtensions.DependencyInjection.cs
index c44d88e363d8..d76f29d7f5fd 100644
--- a/dotnet/src/Connectors/Connectors.Ollama/Extensions/OllamaServiceCollectionExtensions.DependencyInjection.cs
+++ b/dotnet/src/Connectors/Connectors.Ollama/Extensions/OllamaServiceCollectionExtensions.DependencyInjection.cs
@@ -11,10 +11,123 @@
namespace Microsoft.Extensions.DependencyInjection;
///
-/// Extension methods for adding Ollama Text Generation service to the kernel builder.
+/// Extension methods for adding Ollama services to the service collection.
///
public static class OllamaServiceCollectionExtensions
{
+ #region Chat Client
+
+ ///
+ /// Add Ollama Chat Client to the service collection.
+ ///
+ /// The target service collection.
+ /// The model for text generation.
+ /// The endpoint to Ollama hosted service.
+ /// Optional service ID.
+ /// The updated service collection.
+ public static IServiceCollection AddOllamaChatClient(
+ this IServiceCollection services,
+ string modelId,
+ Uri endpoint,
+ string? serviceId = null)
+ {
+ Verify.NotNull(services);
+
+ return services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
+ {
+ var loggerFactory = serviceProvider.GetService();
+
+ var ollamaClient = (IChatClient)new OllamaApiClient(endpoint, modelId);
+
+ var builder = ollamaClient.AsBuilder();
+ if (loggerFactory is not null)
+ {
+ builder.UseLogging(loggerFactory);
+ }
+
+ return builder
+ .UseKernelFunctionInvocation(loggerFactory)
+ .Build(serviceProvider);
+ });
+ }
+
+ ///
+ /// Add Ollama Chat Client to the service collection.
+ ///
+ /// The target service collection.
+ /// The model for text generation.
+ /// Optional custom HttpClient, picked from ServiceCollection if not provided.
+ /// Optional service ID.
+ /// The updated service collection.
+ public static IServiceCollection AddOllamaChatClient(
+ this IServiceCollection services,
+ string modelId,
+ HttpClient? httpClient = null,
+ string? serviceId = null)
+ {
+ Verify.NotNull(services);
+
+ return services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
+ {
+ httpClient ??= HttpClientProvider.GetHttpClient(httpClient, serviceProvider);
+
+ var loggerFactory = serviceProvider.GetService();
+
+ var ollamaClient = (IChatClient)new OllamaApiClient(httpClient, modelId);
+
+ var builder = ollamaClient.AsBuilder();
+ if (loggerFactory is not null)
+ {
+ builder.UseLogging(loggerFactory);
+ }
+
+ return builder
+ .UseKernelFunctionInvocation(loggerFactory)
+ .Build(serviceProvider);
+ });
+ }
+
+ ///
+ /// Add Ollama Chat Client to the service collection.
+ ///
+ /// The target service collection.
+ /// The Ollama Sharp library client.
+ /// The optional service ID.
+ /// The updated service collection.
+ public static IServiceCollection AddOllamaChatClient(
+ this IServiceCollection services,
+ OllamaApiClient? ollamaClient = null,
+ string? serviceId = null)
+ {
+ Verify.NotNull(services);
+
+ return services.AddKeyedSingleton(serviceId, (serviceProvider, _) =>
+ {
+ var loggerFactory = serviceProvider.GetService();
+ ollamaClient ??= serviceProvider.GetKeyedService(serviceId);
+ ollamaClient ??= serviceProvider.GetKeyedService(serviceId) as OllamaApiClient;
+ ollamaClient ??= serviceProvider.GetService();
+ ollamaClient ??= serviceProvider.GetRequiredService() as OllamaApiClient;
+
+ if (ollamaClient is null)
+ {
+ throw new InvalidOperationException($"No {nameof(IOllamaApiClient)} implementations found in the service collection.");
+ }
+
+ var builder = ((IChatClient)ollamaClient).AsBuilder();
+ if (loggerFactory is not null)
+ {
+ builder.UseLogging(loggerFactory);
+ }
+
+ return builder
+ .UseKernelFunctionInvocation(loggerFactory)
+ .Build(serviceProvider);
+ });
+ }
+
+ #endregion
+
#region Text Embeddings
///
diff --git a/dotnet/src/IntegrationTests/Connectors/Ollama/OllamaChatClientIntegrationTests.cs b/dotnet/src/IntegrationTests/Connectors/Ollama/OllamaChatClientIntegrationTests.cs
new file mode 100644
index 000000000000..2a9d577c397e
--- /dev/null
+++ b/dotnet/src/IntegrationTests/Connectors/Ollama/OllamaChatClientIntegrationTests.cs
@@ -0,0 +1,232 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.SemanticKernel;
+using OllamaSharp;
+using Xunit;
+using Xunit.Abstractions;
+using ChatRole = Microsoft.Extensions.AI.ChatRole;
+
+namespace SemanticKernel.IntegrationTests.Connectors.Ollama;
+
+public sealed class OllamaChatClientIntegrationTests : IDisposable
+{
+ private readonly ITestOutputHelper _output;
+ private readonly IConfigurationRoot _configuration;
+
+ public OllamaChatClientIntegrationTests(ITestOutputHelper output)
+ {
+ this._output = output;
+
+ // Load configuration
+ this._configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets()
+ .Build();
+ }
+
+ [Theory(Skip = "This test is for manual verification.")]
+ [InlineData("phi3")]
+ [InlineData("llama3.2")]
+ public async Task OllamaChatClientBasicUsageAsync(string modelId)
+ {
+ // Arrange
+ var endpoint = this._configuration.GetSection("Ollama:Endpoint").Get() ?? "http://localhost:11434";
+ using var ollamaClient = new OllamaApiClient(new Uri(endpoint), modelId);
+ var sut = (IChatClient)ollamaClient;
+
+ var messages = new List
+ {
+ new(ChatRole.User, "What is the capital of France? Answer in one word.")
+ };
+
+ // Act
+ var response = await sut.GetResponseAsync(messages);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotEmpty(response.Text);
+ this._output.WriteLine($"Response: {response.Text}");
+ }
+
+ [Theory(Skip = "This test is for manual verification.")]
+ [InlineData("phi3")]
+ [InlineData("llama3.2")]
+ public async Task OllamaChatClientStreamingUsageAsync(string modelId)
+ {
+ // Arrange
+ var endpoint = this._configuration.GetSection("Ollama:Endpoint").Get() ?? "http://localhost:11434";
+ using var ollamaClient = new OllamaApiClient(new Uri(endpoint), modelId);
+ var sut = (IChatClient)ollamaClient;
+
+ var messages = new List
+ {
+ new(ChatRole.User, "Write a short poem about AI. Keep it under 50 words.")
+ };
+
+ // Act
+ var responseText = "";
+ await foreach (var update in sut.GetStreamingResponseAsync(messages))
+ {
+ if (update.Text != null)
+ {
+ responseText += update.Text;
+ this._output.WriteLine($"Update: {update.Text}");
+ }
+ }
+
+ // Assert
+ Assert.NotEmpty(responseText);
+ this._output.WriteLine($"Complete response: {responseText}");
+ }
+
+ [Theory(Skip = "This test is for manual verification.")]
+ [InlineData("phi3")]
+ public async Task OllamaChatClientWithOptionsAsync(string modelId)
+ {
+ // Arrange
+ var endpoint = this._configuration.GetSection("Ollama:Endpoint").Get() ?? "http://localhost:11434";
+ using var ollamaClient = new OllamaApiClient(new Uri(endpoint), modelId);
+ var sut = (IChatClient)ollamaClient;
+
+ var messages = new List
+ {
+ new(ChatRole.User, "Generate a random number between 1 and 10.")
+ };
+
+ var chatOptions = new ChatOptions
+ {
+ Temperature = 0.1f,
+ MaxOutputTokens = 50
+ };
+
+ // Act
+ var response = await sut.GetResponseAsync(messages, chatOptions);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotEmpty(response.Text);
+ this._output.WriteLine($"Response: {response.Text}");
+ }
+
+ [Fact(Skip = "This test is for manual verification.")]
+ public async Task OllamaChatClientServiceCollectionIntegrationAsync()
+ {
+ // Arrange
+ var endpoint = this._configuration.GetSection("Ollama:Endpoint").Get() ?? "http://localhost:11434";
+ var modelId = "phi3";
+ var serviceId = "test-ollama";
+
+ var services = new ServiceCollection();
+ services.AddOllamaChatClient(modelId, new Uri(endpoint), serviceId);
+ services.AddKernel();
+
+ var serviceProvider = services.BuildServiceProvider();
+ var kernel = serviceProvider.GetRequiredService();
+
+ // Act
+ var chatClient = kernel.GetRequiredService(serviceId);
+ var messages = new List
+ {
+ new(ChatRole.User, "What is 2+2? Answer with just the number.")
+ };
+
+ var response = await chatClient.GetResponseAsync(messages);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotEmpty(response.Text);
+ this._output.WriteLine($"Response: {response.Text}");
+ }
+
+ [Fact(Skip = "This test is for manual verification.")]
+ public async Task OllamaChatClientKernelBuilderIntegrationAsync()
+ {
+ // Arrange
+ var endpoint = this._configuration.GetSection("Ollama:Endpoint").Get() ?? "http://localhost:11434";
+ var modelId = "phi3";
+ var serviceId = "test-ollama";
+
+ var kernel = Kernel.CreateBuilder()
+ .AddOllamaChatClient(modelId, new Uri(endpoint), serviceId)
+ .Build();
+
+ // Act
+ var chatClient = kernel.GetRequiredService(serviceId);
+ var messages = new List
+ {
+ new(ChatRole.User, "What is the largest planet in our solar system? Answer in one word.")
+ };
+
+ var response = await chatClient.GetResponseAsync(messages);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotEmpty(response.Text);
+ this._output.WriteLine($"Response: {response.Text}");
+ }
+
+ [Fact]
+ public void OllamaChatClientMetadataTest()
+ {
+ // Arrange
+ var endpoint = "http://localhost:11434";
+ var modelId = "phi3";
+ using var ollamaClient = new OllamaApiClient(new Uri(endpoint), modelId);
+ var sut = (IChatClient)ollamaClient;
+
+ // Act
+ var metadata = sut.GetService(typeof(ChatClientMetadata)) as ChatClientMetadata;
+
+ // Assert
+ Assert.NotNull(metadata);
+ Assert.Equal(modelId, metadata.DefaultModelId);
+ }
+
+ [Fact(Skip = "This test is for manual verification.")]
+ public async Task OllamaChatClientWithKernelFunctionInvocationAsync()
+ {
+ // Arrange
+ var endpoint = this._configuration.GetSection("Ollama:Endpoint").Get() ?? "http://localhost:11434";
+ var modelId = "llama3.2";
+ var serviceId = "test-ollama";
+
+ var kernel = Kernel.CreateBuilder()
+ .AddOllamaChatClient(modelId, new Uri(endpoint), serviceId)
+ .Build();
+
+ // Add a simple function for testing
+ kernel.Plugins.AddFromFunctions("TestPlugin", [
+ KernelFunctionFactory.CreateFromMethod((string location) =>
+ $"The weather in {location} is sunny with 75°F temperature.",
+ "GetWeather",
+ "Gets the current weather for a location")
+ ]);
+
+ // Act
+ var chatClient = kernel.GetRequiredService(serviceId);
+ var messages = new List
+ {
+ new(ChatRole.User, "What's the weather like in Paris?")
+ };
+
+ var response = await chatClient.GetResponseAsync(messages);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.NotEmpty(response.Text);
+ this._output.WriteLine($"Response: {response.Text}");
+ }
+
+ public void Dispose()
+ {
+ // Cleanup if needed
+ }
+}
From a8b4ee6ab612c14f89a1585c5b682f945437d512 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Mon, 16 Jun 2025 12:14:40 +0100
Subject: [PATCH 2/4] Update samples
---
.../ChatCompletion/Ollama_ChatCompletion.cs | 40 +++++++++----------
.../samples/InternalUtilities/BaseTest.cs | 12 ++++++
2 files changed, 31 insertions(+), 21 deletions(-)
diff --git a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletion.cs b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletion.cs
index 307edbe4b229..1d6ea8fd07b3 100644
--- a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletion.cs
+++ b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletion.cs
@@ -1,10 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
+using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using OllamaSharp;
-using OllamaSharp.Models.Chat;
namespace ChatCompletion;
@@ -17,39 +17,37 @@ public class Ollama_ChatCompletion(ITestOutputHelper output) : BaseTest(output)
/// Demonstrates how you can use the chat completion service directly.
///
[Fact]
- public async Task ServicePromptAsync()
+ public async Task UsingChatClientPromptAsync()
{
Assert.NotNull(TestConfiguration.Ollama.ModelId);
Console.WriteLine("======== Ollama - Chat Completion ========");
- using var ollamaClient = new OllamaApiClient(
+ using IChatClient ollamaClient = new OllamaApiClient(
uriString: TestConfiguration.Ollama.Endpoint,
defaultModel: TestConfiguration.Ollama.ModelId);
- var chatService = ollamaClient.AsChatCompletionService();
-
Console.WriteLine("Chat content:");
Console.WriteLine("------------------------");
- var chatHistory = new ChatHistory("You are a librarian, expert about books");
+ List chatHistory = [new ChatMessage(ChatRole.System, "You are a librarian, expert about books")];
// First user message
- chatHistory.AddUserMessage("Hi, I'm looking for book suggestions");
+ chatHistory.Add(new(ChatRole.User, "Hi, I'm looking for book suggestions"));
this.OutputLastMessage(chatHistory);
// First assistant message
- var reply = await chatService.GetChatMessageContentAsync(chatHistory);
- chatHistory.Add(reply);
+ var reply = await ollamaClient.GetResponseAsync(chatHistory);
+ chatHistory.AddRange(reply.Messages);
this.OutputLastMessage(chatHistory);
// Second user message
- chatHistory.AddUserMessage("I love history and philosophy, I'd like to learn something new about Greece, any suggestion");
+ chatHistory.Add(new(ChatRole.User, "I love history and philosophy, I'd like to learn something new about Greece, any suggestion"));
this.OutputLastMessage(chatHistory);
// Second assistant message
- reply = await chatService.GetChatMessageContentAsync(chatHistory);
- chatHistory.Add(reply);
+ reply = await ollamaClient.GetResponseAsync(chatHistory);
+ chatHistory.AddRange(reply.Messages);
this.OutputLastMessage(chatHistory);
}
@@ -61,7 +59,7 @@ public async Task ServicePromptAsync()
/// may cause breaking changes in the code below.
///
[Fact]
- public async Task ServicePromptWithInnerContentAsync()
+ public async Task UsingChatCompletionServicePromptWithInnerContentAsync()
{
Assert.NotNull(TestConfiguration.Ollama.ModelId);
@@ -87,9 +85,9 @@ public async Task ServicePromptWithInnerContentAsync()
// Assistant message details
// Ollama Sharp does not support non-streaming and always perform streaming calls, for this reason, the inner content is always a list of chunks.
- var replyInnerContent = reply.InnerContent as ChatDoneResponseStream;
+ var ollamaSharpInnerContent = reply.InnerContent as OllamaSharp.Models.Chat.ChatDoneResponseStream;
- OutputInnerContent(replyInnerContent!);
+ OutputOllamaSharpContent(ollamaSharpInnerContent!);
}
///
@@ -106,7 +104,7 @@ public async Task ChatPromptAsync()
""");
var kernel = Kernel.CreateBuilder()
- .AddOllamaChatCompletion(
+ .AddOllamaChatClient(
endpoint: new Uri(TestConfiguration.Ollama.Endpoint ?? "http://localhost:11434"),
modelId: TestConfiguration.Ollama.ModelId)
.Build();
@@ -139,7 +137,7 @@ public async Task ChatPromptWithInnerContentAsync()
""");
var kernel = Kernel.CreateBuilder()
- .AddOllamaChatCompletion(
+ .AddOllamaChatClient(
endpoint: new Uri(TestConfiguration.Ollama.Endpoint ?? "http://localhost:11434"),
modelId: TestConfiguration.Ollama.ModelId)
.Build();
@@ -147,10 +145,10 @@ public async Task ChatPromptWithInnerContentAsync()
var functionResult = await kernel.InvokePromptAsync(chatPrompt.ToString());
// Ollama Sharp does not support non-streaming and always perform streaming calls, for this reason, the inner content of a non-streaming result is a list of the generated chunks.
- var messageContent = functionResult.GetValue(); // Retrieves underlying chat message content from FunctionResult.
- var replyInnerContent = messageContent!.InnerContent as ChatDoneResponseStream; // Retrieves inner content from ChatMessageContent.
+ var messageContent = functionResult.GetValue(); // Retrieves underlying chat message content from FunctionResult.
+ var ollamaSharpRawRepresentation = messageContent!.RawRepresentation as OllamaSharp.Models.Chat.ChatDoneResponseStream; // Retrieves inner content from ChatMessageContent.
- OutputInnerContent(replyInnerContent!);
+ OutputOllamaSharpContent(ollamaSharpRawRepresentation!);
}
///
@@ -161,7 +159,7 @@ public async Task ChatPromptWithInnerContentAsync()
/// This is a breaking glass scenario, any attempt on running with different versions of OllamaSharp library that introduces breaking changes
/// may cause breaking changes in the code below.
///
- private void OutputInnerContent(ChatDoneResponseStream innerContent)
+ private void OutputOllamaSharpContent(OllamaSharp.Models.Chat.ChatDoneResponseStream innerContent)
{
Console.WriteLine($$"""
Model: {{innerContent.Model}}
diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs
index 86f81ceb982a..9f2b57009392 100644
--- a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs
+++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs
@@ -189,6 +189,18 @@ protected void OutputLastMessage(ChatHistory chatHistory)
Console.WriteLine("------------------------");
}
+ ///
+ /// Outputs the last message in the chat messages history.
+ ///
+ /// Chat messages history
+ protected void OutputLastMessage(IReadOnlyCollection chatHistory)
+ {
+ var message = chatHistory.Last();
+
+ Console.WriteLine($"{message.Role}: {message.Text}");
+ Console.WriteLine("------------------------");
+ }
+
///
/// Outputs out the stream of generated message tokens.
///
From a333f16241f733f5386c78805cca8ca563513a13 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Mon, 16 Jun 2025 11:44:56 +0000
Subject: [PATCH 3/4] WIP
---
.../Ollama_ChatCompletionStreaming.cs | 69 ++++++++++++++++++-
.../Ollama_ChatCompletionWithVision.cs | 32 +++++++++
2 files changed, 99 insertions(+), 2 deletions(-)
diff --git a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs
index 1713d9a03052..e4f590e6c911 100644
--- a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs
+++ b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
+using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using OllamaSharp;
@@ -13,6 +14,41 @@ namespace ChatCompletion;
///
public class Ollama_ChatCompletionStreaming(ITestOutputHelper output) : BaseTest(output)
{
+ ///
+ /// This example demonstrates chat completion streaming using IChatClient directly.
+ ///
+ [Fact]
+ public async Task UsingChatClientStreamingWithOllama()
+ {
+ Assert.NotNull(TestConfiguration.Ollama.ModelId);
+
+ Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingChatClientStreamingWithOllama)} ========");
+
+ using IChatClient ollamaClient = new OllamaApiClient(
+ uriString: TestConfiguration.Ollama.Endpoint,
+ defaultModel: TestConfiguration.Ollama.ModelId);
+
+ Console.WriteLine("Chat content:");
+ Console.WriteLine("------------------------");
+
+ List chatHistory = [new Microsoft.Extensions.AI.ChatMessage(Microsoft.Extensions.AI.ChatRole.System, "You are a librarian, expert about books")];
+ this.OutputLastMessage(chatHistory);
+
+ // First user message
+ chatHistory.Add(new(Microsoft.Extensions.AI.ChatRole.User, "Hi, I'm looking for book suggestions"));
+ this.OutputLastMessage(chatHistory);
+
+ // First assistant message
+ await StreamChatClientMessageOutputAsync(ollamaClient, chatHistory);
+
+ // Second user message
+ chatHistory.Add(new(Microsoft.Extensions.AI.ChatRole.User, "I love history and philosophy, I'd like to learn something new about Greece, any suggestion?"));
+ this.OutputLastMessage(chatHistory);
+
+ // Second assistant message
+ await StreamChatClientMessageOutputAsync(ollamaClient, chatHistory);
+ }
+
///
/// This example demonstrates chat completion streaming using Ollama.
///
@@ -102,7 +138,7 @@ public async Task UsingKernelChatPromptStreamingWithOllama()
""");
var kernel = Kernel.CreateBuilder()
- .AddOllamaChatCompletion(
+ .AddOllamaChatClient(
endpoint: new Uri(TestConfiguration.Ollama.Endpoint),
modelId: TestConfiguration.Ollama.ModelId)
.Build();
@@ -136,7 +172,7 @@ public async Task UsingKernelChatPromptStreamingInnerContentsWithOllama()
""");
var kernel = Kernel.CreateBuilder()
- .AddOllamaChatCompletion(
+ .AddOllamaChatClient(
endpoint: new Uri(TestConfiguration.Ollama.Endpoint),
modelId: TestConfiguration.Ollama.ModelId)
.Build();
@@ -245,4 +281,33 @@ private void OutputInnerContent(ChatResponseStream streamChunk)
}
Console.WriteLine("------------------------");
}
+
+ private async Task StreamChatClientMessageOutputAsync(IChatClient chatClient, List chatHistory)
+ {
+ bool roleWritten = false;
+ string fullMessage = string.Empty;
+ List chatUpdates = [];
+ await foreach (var chatUpdate in chatClient.GetStreamingResponseAsync(chatHistory))
+ {
+ chatUpdates.Add(chatUpdate);
+ if (!roleWritten && !string.IsNullOrEmpty(chatUpdate.Text))
+ {
+ Console.Write($"Assistant: {chatUpdate.Text}");
+ roleWritten = true;
+ }
+ else if (!string.IsNullOrEmpty(chatUpdate.Text))
+ {
+ Console.Write(chatUpdate.Text);
+ }
+ }
+
+ Console.WriteLine("\n------------------------");
+ chatHistory.Add(chatUpdates.To);
+ }
+
+ private void OutputLastMessage(List chatHistory)
+ {
+ var message = chatHistory.Last();
+ Console.WriteLine($"{message.Role}: {message.Text}");
+ }
}
diff --git a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionWithVision.cs b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionWithVision.cs
index 0e979733cd1f..b8358c37ab75 100644
--- a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionWithVision.cs
+++ b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionWithVision.cs
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
+using OllamaSharp;
using Resources;
namespace ChatCompletion;
@@ -11,6 +13,36 @@ namespace ChatCompletion;
///
public class Ollama_ChatCompletionWithVision(ITestOutputHelper output) : BaseTest(output)
{
+ ///
+ /// This sample uses IChatClient directly with a local image file and sends it to the model along
+ /// with a text message to get the description of the image.
+ ///
+ [Fact]
+ public async Task GetLocalImageDescriptionUsingChatClient()
+ {
+ Console.WriteLine($"======== Ollama - {nameof(GetLocalImageDescriptionUsingChatClient)} ========");
+
+ var imageBytes = await EmbeddedResource.ReadAllAsync("sample_image.jpg");
+
+ var kernel = Kernel.CreateBuilder()
+ .AddOllamaChatClient(modelId: "llama3.2-vision", endpoint: new Uri(TestConfiguration.Ollama.Endpoint))
+ .Build();
+
+ var chatClient = kernel.GetRequiredService();
+
+ List chatHistory = [
+ new(ChatRole.System, "You are a friendly assistant."),
+ new(ChatRole.User, [
+ new Microsoft.Extensions.AI.TextContent("What's in this image?"),
+ new Microsoft.Extensions.AI.DataContent(imageBytes, "image/jpg")
+ ])
+ ];
+
+ var response = await chatClient.GetResponseAsync(chatHistory);
+
+ Console.WriteLine(response.Text);
+ }
+
///
/// This sample uses a local image file and sends it to the model along
/// with a text message the get the description of the image.
From 1c9d913c5422401c8e5df1a17f7a5389d5b12652 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Mon, 16 Jun 2025 14:49:50 +0100
Subject: [PATCH 4/4] Address ChatPrompt support for streaming API's and added
missing Ollama Concept demos
---
.../Ollama_ChatCompletionStreaming.cs | 107 +++++++++---------
.../Ollama_ChatCompletionWithVision.cs | 2 +-
.../AI/ChatClient/ChatClientExtensions.cs | 8 ++
3 files changed, 61 insertions(+), 56 deletions(-)
diff --git a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs
index e4f590e6c911..493003772153 100644
--- a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs
+++ b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs
@@ -5,7 +5,6 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using OllamaSharp;
-using OllamaSharp.Models.Chat;
namespace ChatCompletion;
@@ -15,14 +14,14 @@ namespace ChatCompletion;
public class Ollama_ChatCompletionStreaming(ITestOutputHelper output) : BaseTest(output)
{
///
- /// This example demonstrates chat completion streaming using IChatClient directly.
+ /// This example demonstrates chat completion streaming using directly.
///
[Fact]
- public async Task UsingChatClientStreamingWithOllama()
+ public async Task UsingChatClientStreaming()
{
Assert.NotNull(TestConfiguration.Ollama.ModelId);
- Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingChatClientStreamingWithOllama)} ========");
+ Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingChatClientStreaming)} ========");
using IChatClient ollamaClient = new OllamaApiClient(
uriString: TestConfiguration.Ollama.Endpoint,
@@ -31,11 +30,11 @@ public async Task UsingChatClientStreamingWithOllama()
Console.WriteLine("Chat content:");
Console.WriteLine("------------------------");
- List chatHistory = [new Microsoft.Extensions.AI.ChatMessage(Microsoft.Extensions.AI.ChatRole.System, "You are a librarian, expert about books")];
+ List chatHistory = [new ChatMessage(ChatRole.System, "You are a librarian, expert about books")];
this.OutputLastMessage(chatHistory);
// First user message
- chatHistory.Add(new(Microsoft.Extensions.AI.ChatRole.User, "Hi, I'm looking for book suggestions"));
+ chatHistory.Add(new(ChatRole.User, "Hi, I'm looking for book suggestions"));
this.OutputLastMessage(chatHistory);
// First assistant message
@@ -50,14 +49,14 @@ public async Task UsingChatClientStreamingWithOllama()
}
///
- /// This example demonstrates chat completion streaming using Ollama.
+ /// This example demonstrates chat completion streaming using directly.
///
[Fact]
- public async Task UsingServiceStreamingWithOllama()
+ public async Task UsingChatCompletionServiceStreamingWithOllama()
{
Assert.NotNull(TestConfiguration.Ollama.ModelId);
- Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingServiceStreamingWithOllama)} ========");
+ Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingChatCompletionServiceStreamingWithOllama)} ========");
using var ollamaClient = new OllamaApiClient(
uriString: TestConfiguration.Ollama.Endpoint,
@@ -87,38 +86,36 @@ public async Task UsingServiceStreamingWithOllama()
}
///
- /// This example demonstrates retrieving underlying library information through chat completion streaming inner contents.
+ /// This example demonstrates retrieving underlying OllamaSharp library information through streaming raw representation (breaking glass) approach.
///
///
/// This is a breaking glass scenario and is more susceptible to break on newer versions of OllamaSharp library.
///
[Fact]
- public async Task UsingServiceStreamingInnerContentsWithOllama()
+ public async Task UsingChatClientStreamingRawContentsWithOllama()
{
Assert.NotNull(TestConfiguration.Ollama.ModelId);
- Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingServiceStreamingInnerContentsWithOllama)} ========");
+ Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingChatClientStreamingRawContentsWithOllama)} ========");
- using var ollamaClient = new OllamaApiClient(
+ using IChatClient ollamaClient = new OllamaApiClient(
uriString: TestConfiguration.Ollama.Endpoint,
defaultModel: TestConfiguration.Ollama.ModelId);
- var chatService = ollamaClient.AsChatCompletionService();
-
Console.WriteLine("Chat content:");
Console.WriteLine("------------------------");
- var chatHistory = new ChatHistory("You are a librarian, expert about books");
+ List chatHistory = [new ChatMessage(ChatRole.System, "You are a librarian, expert about books")];
this.OutputLastMessage(chatHistory);
// First user message
- chatHistory.AddUserMessage("Hi, I'm looking for book suggestions");
+ chatHistory.Add(new(ChatRole.User, "Hi, I'm looking for book suggestions"));
this.OutputLastMessage(chatHistory);
- await foreach (var chatUpdate in chatService.GetStreamingChatMessageContentsAsync(chatHistory))
+ await foreach (var chatUpdate in ollamaClient.GetStreamingResponseAsync(chatHistory))
{
- var innerContent = chatUpdate.InnerContent as ChatResponseStream;
- OutputInnerContent(innerContent!);
+ var rawRepresentation = chatUpdate.RawRepresentation as OllamaSharp.Models.Chat.ChatResponseStream;
+ OutputOllamaSharpContent(rawRepresentation!);
}
}
@@ -126,11 +123,11 @@ public async Task UsingServiceStreamingInnerContentsWithOllama()
/// Demonstrates how you can template a chat history call while using the for invocation.
///
[Fact]
- public async Task UsingKernelChatPromptStreamingWithOllama()
+ public async Task UsingKernelChatPromptStreaming()
{
Assert.NotNull(TestConfiguration.Ollama.ModelId);
- Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingKernelChatPromptStreamingWithOllama)} ========");
+ Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingKernelChatPromptStreaming)} ========");
StringBuilder chatPrompt = new("""
You are a librarian, expert about books
@@ -160,11 +157,11 @@ public async Task UsingKernelChatPromptStreamingWithOllama()
/// This is a breaking glass scenario and is more susceptible to break on newer versions of OllamaSharp library.
///
[Fact]
- public async Task UsingKernelChatPromptStreamingInnerContentsWithOllama()
+ public async Task UsingKernelChatPromptStreamingRawRepresentation()
{
Assert.NotNull(TestConfiguration.Ollama.ModelId);
- Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingKernelChatPromptStreamingInnerContentsWithOllama)} ========");
+ Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingKernelChatPromptStreamingRawRepresentation)} ========");
StringBuilder chatPrompt = new("""
You are a librarian, expert about books
@@ -184,8 +181,8 @@ public async Task UsingKernelChatPromptStreamingInnerContentsWithOllama()
await foreach (var chatUpdate in kernel.InvokePromptStreamingAsync(chatPrompt.ToString()))
{
- var innerContent = chatUpdate.InnerContent as ChatResponseStream;
- OutputInnerContent(innerContent!);
+ var innerContent = chatUpdate.InnerContent as OllamaSharp.Models.Chat.ChatResponseStream;
+ OutputOllamaSharpContent(innerContent!);
}
}
@@ -195,11 +192,11 @@ public async Task UsingKernelChatPromptStreamingInnerContentsWithOllama()
/// and alternatively via the StreamingChatMessageContent.Items property.
///
[Fact]
- public async Task UsingStreamingTextFromChatCompletionWithOllama()
+ public async Task UsingStreamingTextFromChatCompletion()
{
Assert.NotNull(TestConfiguration.Ollama.ModelId);
- Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingStreamingTextFromChatCompletionWithOllama)} ========");
+ Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingStreamingTextFromChatCompletion)} ========");
using var ollamaClient = new OllamaApiClient(
uriString: TestConfiguration.Ollama.Endpoint,
@@ -248,6 +245,29 @@ private async Task StreamMessageOutputFromKernelAsync(Kernel kernel, str
return fullMessage;
}
+ private async Task StreamChatClientMessageOutputAsync(IChatClient chatClient, List chatHistory)
+ {
+ bool roleWritten = false;
+ string fullMessage = string.Empty;
+ List chatUpdates = [];
+ await foreach (var chatUpdate in chatClient.GetStreamingResponseAsync(chatHistory))
+ {
+ chatUpdates.Add(chatUpdate);
+ if (!roleWritten && !string.IsNullOrEmpty(chatUpdate.Text))
+ {
+ Console.Write($"Assistant: {chatUpdate.Text}");
+ roleWritten = true;
+ }
+ else if (!string.IsNullOrEmpty(chatUpdate.Text))
+ {
+ Console.Write(chatUpdate.Text);
+ }
+ }
+
+ Console.WriteLine("\n------------------------");
+ chatHistory.AddRange(chatUpdates.ToChatResponse().Messages);
+ }
+
///
/// Retrieve extra information from each streaming chunk response.
///
@@ -256,7 +276,7 @@ private async Task StreamMessageOutputFromKernelAsync(Kernel kernel, str
/// This is a breaking glass scenario, any attempt on running with different versions of OllamaSharp library that introduces breaking changes
/// may cause breaking changes in the code below.
///
- private void OutputInnerContent(ChatResponseStream streamChunk)
+ private void OutputOllamaSharpContent(OllamaSharp.Models.Chat.ChatResponseStream streamChunk)
{
Console.WriteLine($$"""
Model: {{streamChunk.Model}}
@@ -266,8 +286,8 @@ private void OutputInnerContent(ChatResponseStream streamChunk)
Done: {{streamChunk.Done}}
""");
- /// The last message in the chunk is a type with additional metadata.
- if (streamChunk is ChatDoneResponseStream doneStream)
+ /// The last message in the chunk is a type with additional metadata.
+ if (streamChunk is OllamaSharp.Models.Chat.ChatDoneResponseStream doneStream)
{
Console.WriteLine($$"""
Done Reason: {{doneStream.DoneReason}}
@@ -282,30 +302,7 @@ private void OutputInnerContent(ChatResponseStream streamChunk)
Console.WriteLine("------------------------");
}
- private async Task StreamChatClientMessageOutputAsync(IChatClient chatClient, List chatHistory)
- {
- bool roleWritten = false;
- string fullMessage = string.Empty;
- List chatUpdates = [];
- await foreach (var chatUpdate in chatClient.GetStreamingResponseAsync(chatHistory))
- {
- chatUpdates.Add(chatUpdate);
- if (!roleWritten && !string.IsNullOrEmpty(chatUpdate.Text))
- {
- Console.Write($"Assistant: {chatUpdate.Text}");
- roleWritten = true;
- }
- else if (!string.IsNullOrEmpty(chatUpdate.Text))
- {
- Console.Write(chatUpdate.Text);
- }
- }
-
- Console.WriteLine("\n------------------------");
- chatHistory.Add(chatUpdates.To);
- }
-
- private void OutputLastMessage(List chatHistory)
+ private void OutputLastMessage(List chatHistory)
{
var message = chatHistory.Last();
Console.WriteLine($"{message.Role}: {message.Text}");
diff --git a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionWithVision.cs b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionWithVision.cs
index b8358c37ab75..2b63e714e3a9 100644
--- a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionWithVision.cs
+++ b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionWithVision.cs
@@ -3,8 +3,8 @@
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
-using OllamaSharp;
using Resources;
+using TextContent = Microsoft.SemanticKernel.TextContent;
namespace ChatCompletion;
diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/ChatClientExtensions.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/ChatClientExtensions.cs
index cac719649cab..833be1812c60 100644
--- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/ChatClientExtensions.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/ChatClientExtensions.cs
@@ -57,6 +57,14 @@ internal static IAsyncEnumerable GetStreamingResponseAsync(
{
var chatOptions = executionSettings.ToChatOptions(kernel);
+ // Try to parse the text as a chat history
+ if (ChatPromptParser.TryParse(prompt, out var chatHistoryFromPrompt))
+ {
+ var messageList = chatHistoryFromPrompt.ToChatMessageList();
+ return chatClient.GetStreamingResponseAsync(messageList, chatOptions, cancellationToken);
+ }
+
+ // Otherwise, use the prompt as the chat user message
return chatClient.GetStreamingResponseAsync(prompt, chatOptions, cancellationToken);
}