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/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs index 1713d9a03052..493003772153 100644 --- a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs +++ b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.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; @@ -14,14 +14,49 @@ namespace ChatCompletion; public class Ollama_ChatCompletionStreaming(ITestOutputHelper output) : BaseTest(output) { /// - /// This example demonstrates chat completion streaming using Ollama. + /// This example demonstrates chat completion streaming using directly. /// [Fact] - public async Task UsingServiceStreamingWithOllama() + public async Task UsingChatClientStreaming() { Assert.NotNull(TestConfiguration.Ollama.ModelId); - Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingServiceStreamingWithOllama)} ========"); + Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingChatClientStreaming)} ========"); + + using IChatClient ollamaClient = new OllamaApiClient( + uriString: TestConfiguration.Ollama.Endpoint, + defaultModel: TestConfiguration.Ollama.ModelId); + + Console.WriteLine("Chat content:"); + Console.WriteLine("------------------------"); + + List chatHistory = [new ChatMessage(ChatRole.System, "You are a librarian, expert about books")]; + this.OutputLastMessage(chatHistory); + + // First user message + chatHistory.Add(new(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 directly. + /// + [Fact] + public async Task UsingChatCompletionServiceStreamingWithOllama() + { + Assert.NotNull(TestConfiguration.Ollama.ModelId); + + Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingChatCompletionServiceStreamingWithOllama)} ========"); using var ollamaClient = new OllamaApiClient( uriString: TestConfiguration.Ollama.Endpoint, @@ -51,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!); } } @@ -90,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 @@ -102,7 +135,7 @@ public async Task UsingKernelChatPromptStreamingWithOllama() """); var kernel = Kernel.CreateBuilder() - .AddOllamaChatCompletion( + .AddOllamaChatClient( endpoint: new Uri(TestConfiguration.Ollama.Endpoint), modelId: TestConfiguration.Ollama.ModelId) .Build(); @@ -124,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 @@ -136,7 +169,7 @@ public async Task UsingKernelChatPromptStreamingInnerContentsWithOllama() """); var kernel = Kernel.CreateBuilder() - .AddOllamaChatCompletion( + .AddOllamaChatClient( endpoint: new Uri(TestConfiguration.Ollama.Endpoint), modelId: TestConfiguration.Ollama.ModelId) .Build(); @@ -148,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!); } } @@ -159,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, @@ -212,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. /// @@ -220,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}} @@ -230,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}} @@ -245,4 +301,10 @@ private void OutputInnerContent(ChatResponseStream streamChunk) } Console.WriteLine("------------------------"); } + + 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..2b63e714e3a9 100644 --- a/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionWithVision.cs +++ b/dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionWithVision.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.Extensions.AI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Resources; +using TextContent = Microsoft.SemanticKernel.TextContent; 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. 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 + } +} 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. /// 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); }