Skip to content

Commit bbc66df

Browse files
authored
.Net: Add Ollama ChatClient Extensions + UT (#12476)
### Motivation and Context Successfully implemented the missing AddOllamaChatClient extensions for the Ollama Connector with comprehensive testing.: ## Key Features Implemented Same Signature Patterns: All extensions follow the exact same parameter signatures as the existing IChatCompletionService extensions IChatClient Target: Returns IChatClient instead of IChatCompletionService Function Calling Support: Includes UseKernelFunctionInvocation() for function calling capabilities Comprehensive Testing: Full test coverage for all scenarios including error handling, service registration, and functionality Integration Ready: Integration tests are ready to run when secrets are configured - ### Implementation - Extension Methods Added - IServiceCollection Extensions: 3 overloads in OllamaServiceCollectionExtensions.DependencyInjection.cs - IKernelBuilder Extensions: 3 overloads in OllamaKernelBuilderExtensions.cs - ### Unit Tests Created: - OllamaServiceCollectionExtensionsChatClientTests.cs: 7 tests for service collection extensions - OllamaKernelBuilderExtensionsChatClientTests.cs: 6 tests for kernel builder extensions - OllamaChatClientTests.cs: 12 tests for IChatClient functionality - ### Integration Tests Created: - OllamaChatClientIntegrationTests.cs: 7 integration tests
1 parent c07711e commit bbc66df

File tree

11 files changed

+1118
-50
lines changed

11 files changed

+1118
-50
lines changed

dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletion.cs

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System.Text;
4+
using Microsoft.Extensions.AI;
45
using Microsoft.SemanticKernel;
56
using Microsoft.SemanticKernel.ChatCompletion;
67
using OllamaSharp;
7-
using OllamaSharp.Models.Chat;
88

99
namespace ChatCompletion;
1010

@@ -17,39 +17,37 @@ public class Ollama_ChatCompletion(ITestOutputHelper output) : BaseTest(output)
1717
/// Demonstrates how you can use the chat completion service directly.
1818
/// </summary>
1919
[Fact]
20-
public async Task ServicePromptAsync()
20+
public async Task UsingChatClientPromptAsync()
2121
{
2222
Assert.NotNull(TestConfiguration.Ollama.ModelId);
2323

2424
Console.WriteLine("======== Ollama - Chat Completion ========");
2525

26-
using var ollamaClient = new OllamaApiClient(
26+
using IChatClient ollamaClient = new OllamaApiClient(
2727
uriString: TestConfiguration.Ollama.Endpoint,
2828
defaultModel: TestConfiguration.Ollama.ModelId);
2929

30-
var chatService = ollamaClient.AsChatCompletionService();
31-
3230
Console.WriteLine("Chat content:");
3331
Console.WriteLine("------------------------");
3432

35-
var chatHistory = new ChatHistory("You are a librarian, expert about books");
33+
List<ChatMessage> chatHistory = [new ChatMessage(ChatRole.System, "You are a librarian, expert about books")];
3634

3735
// First user message
38-
chatHistory.AddUserMessage("Hi, I'm looking for book suggestions");
36+
chatHistory.Add(new(ChatRole.User, "Hi, I'm looking for book suggestions"));
3937
this.OutputLastMessage(chatHistory);
4038

4139
// First assistant message
42-
var reply = await chatService.GetChatMessageContentAsync(chatHistory);
43-
chatHistory.Add(reply);
40+
var reply = await ollamaClient.GetResponseAsync(chatHistory);
41+
chatHistory.AddRange(reply.Messages);
4442
this.OutputLastMessage(chatHistory);
4543

4644
// Second user message
47-
chatHistory.AddUserMessage("I love history and philosophy, I'd like to learn something new about Greece, any suggestion");
45+
chatHistory.Add(new(ChatRole.User, "I love history and philosophy, I'd like to learn something new about Greece, any suggestion"));
4846
this.OutputLastMessage(chatHistory);
4947

5048
// Second assistant message
51-
reply = await chatService.GetChatMessageContentAsync(chatHistory);
52-
chatHistory.Add(reply);
49+
reply = await ollamaClient.GetResponseAsync(chatHistory);
50+
chatHistory.AddRange(reply.Messages);
5351
this.OutputLastMessage(chatHistory);
5452
}
5553

@@ -61,7 +59,7 @@ public async Task ServicePromptAsync()
6159
/// may cause breaking changes in the code below.
6260
/// </remarks>
6361
[Fact]
64-
public async Task ServicePromptWithInnerContentAsync()
62+
public async Task UsingChatCompletionServicePromptWithInnerContentAsync()
6563
{
6664
Assert.NotNull(TestConfiguration.Ollama.ModelId);
6765

@@ -87,9 +85,9 @@ public async Task ServicePromptWithInnerContentAsync()
8785

8886
// Assistant message details
8987
// Ollama Sharp does not support non-streaming and always perform streaming calls, for this reason, the inner content is always a list of chunks.
90-
var replyInnerContent = reply.InnerContent as ChatDoneResponseStream;
88+
var ollamaSharpInnerContent = reply.InnerContent as OllamaSharp.Models.Chat.ChatDoneResponseStream;
9189

92-
OutputInnerContent(replyInnerContent!);
90+
OutputOllamaSharpContent(ollamaSharpInnerContent!);
9391
}
9492

9593
/// <summary>
@@ -106,7 +104,7 @@ public async Task ChatPromptAsync()
106104
""");
107105

108106
var kernel = Kernel.CreateBuilder()
109-
.AddOllamaChatCompletion(
107+
.AddOllamaChatClient(
110108
endpoint: new Uri(TestConfiguration.Ollama.Endpoint ?? "http://localhost:11434"),
111109
modelId: TestConfiguration.Ollama.ModelId)
112110
.Build();
@@ -139,18 +137,18 @@ public async Task ChatPromptWithInnerContentAsync()
139137
""");
140138

141139
var kernel = Kernel.CreateBuilder()
142-
.AddOllamaChatCompletion(
140+
.AddOllamaChatClient(
143141
endpoint: new Uri(TestConfiguration.Ollama.Endpoint ?? "http://localhost:11434"),
144142
modelId: TestConfiguration.Ollama.ModelId)
145143
.Build();
146144

147145
var functionResult = await kernel.InvokePromptAsync(chatPrompt.ToString());
148146

149147
// 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.
150-
var messageContent = functionResult.GetValue<ChatMessageContent>(); // Retrieves underlying chat message content from FunctionResult.
151-
var replyInnerContent = messageContent!.InnerContent as ChatDoneResponseStream; // Retrieves inner content from ChatMessageContent.
148+
var messageContent = functionResult.GetValue<ChatResponse>(); // Retrieves underlying chat message content from FunctionResult.
149+
var ollamaSharpRawRepresentation = messageContent!.RawRepresentation as OllamaSharp.Models.Chat.ChatDoneResponseStream; // Retrieves inner content from ChatMessageContent.
152150

153-
OutputInnerContent(replyInnerContent!);
151+
OutputOllamaSharpContent(ollamaSharpRawRepresentation!);
154152
}
155153

156154
/// <summary>
@@ -161,7 +159,7 @@ public async Task ChatPromptWithInnerContentAsync()
161159
/// This is a breaking glass scenario, any attempt on running with different versions of OllamaSharp library that introduces breaking changes
162160
/// may cause breaking changes in the code below.
163161
/// </remarks>
164-
private void OutputInnerContent(ChatDoneResponseStream innerContent)
162+
private void OutputOllamaSharpContent(OllamaSharp.Models.Chat.ChatDoneResponseStream innerContent)
165163
{
166164
Console.WriteLine($$"""
167165
Model: {{innerContent.Model}}

dotnet/samples/Concepts/ChatCompletion/Ollama_ChatCompletionStreaming.cs

Lines changed: 90 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

33
using System.Text;
4+
using Microsoft.Extensions.AI;
45
using Microsoft.SemanticKernel;
56
using Microsoft.SemanticKernel.ChatCompletion;
67
using OllamaSharp;
7-
using OllamaSharp.Models.Chat;
88

99
namespace ChatCompletion;
1010

@@ -14,14 +14,49 @@ namespace ChatCompletion;
1414
public class Ollama_ChatCompletionStreaming(ITestOutputHelper output) : BaseTest(output)
1515
{
1616
/// <summary>
17-
/// This example demonstrates chat completion streaming using Ollama.
17+
/// This example demonstrates chat completion streaming using <see cref="IChatClient"/> directly.
1818
/// </summary>
1919
[Fact]
20-
public async Task UsingServiceStreamingWithOllama()
20+
public async Task UsingChatClientStreaming()
2121
{
2222
Assert.NotNull(TestConfiguration.Ollama.ModelId);
2323

24-
Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingServiceStreamingWithOllama)} ========");
24+
Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingChatClientStreaming)} ========");
25+
26+
using IChatClient ollamaClient = new OllamaApiClient(
27+
uriString: TestConfiguration.Ollama.Endpoint,
28+
defaultModel: TestConfiguration.Ollama.ModelId);
29+
30+
Console.WriteLine("Chat content:");
31+
Console.WriteLine("------------------------");
32+
33+
List<ChatMessage> chatHistory = [new ChatMessage(ChatRole.System, "You are a librarian, expert about books")];
34+
this.OutputLastMessage(chatHistory);
35+
36+
// First user message
37+
chatHistory.Add(new(ChatRole.User, "Hi, I'm looking for book suggestions"));
38+
this.OutputLastMessage(chatHistory);
39+
40+
// First assistant message
41+
await StreamChatClientMessageOutputAsync(ollamaClient, chatHistory);
42+
43+
// Second user message
44+
chatHistory.Add(new(Microsoft.Extensions.AI.ChatRole.User, "I love history and philosophy, I'd like to learn something new about Greece, any suggestion?"));
45+
this.OutputLastMessage(chatHistory);
46+
47+
// Second assistant message
48+
await StreamChatClientMessageOutputAsync(ollamaClient, chatHistory);
49+
}
50+
51+
/// <summary>
52+
/// This example demonstrates chat completion streaming using <see cref="IChatCompletionService"/> directly.
53+
/// </summary>
54+
[Fact]
55+
public async Task UsingChatCompletionServiceStreamingWithOllama()
56+
{
57+
Assert.NotNull(TestConfiguration.Ollama.ModelId);
58+
59+
Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingChatCompletionServiceStreamingWithOllama)} ========");
2560

2661
using var ollamaClient = new OllamaApiClient(
2762
uriString: TestConfiguration.Ollama.Endpoint,
@@ -51,58 +86,56 @@ public async Task UsingServiceStreamingWithOllama()
5186
}
5287

5388
/// <summary>
54-
/// This example demonstrates retrieving underlying library information through chat completion streaming inner contents.
89+
/// This example demonstrates retrieving underlying OllamaSharp library information through <see cref="IChatClient" /> streaming raw representation (breaking glass) approach.
5590
/// </summary>
5691
/// <remarks>
5792
/// This is a breaking glass scenario and is more susceptible to break on newer versions of OllamaSharp library.
5893
/// </remarks>
5994
[Fact]
60-
public async Task UsingServiceStreamingInnerContentsWithOllama()
95+
public async Task UsingChatClientStreamingRawContentsWithOllama()
6196
{
6297
Assert.NotNull(TestConfiguration.Ollama.ModelId);
6398

64-
Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingServiceStreamingInnerContentsWithOllama)} ========");
99+
Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingChatClientStreamingRawContentsWithOllama)} ========");
65100

66-
using var ollamaClient = new OllamaApiClient(
101+
using IChatClient ollamaClient = new OllamaApiClient(
67102
uriString: TestConfiguration.Ollama.Endpoint,
68103
defaultModel: TestConfiguration.Ollama.ModelId);
69104

70-
var chatService = ollamaClient.AsChatCompletionService();
71-
72105
Console.WriteLine("Chat content:");
73106
Console.WriteLine("------------------------");
74107

75-
var chatHistory = new ChatHistory("You are a librarian, expert about books");
108+
List<ChatMessage> chatHistory = [new ChatMessage(ChatRole.System, "You are a librarian, expert about books")];
76109
this.OutputLastMessage(chatHistory);
77110

78111
// First user message
79-
chatHistory.AddUserMessage("Hi, I'm looking for book suggestions");
112+
chatHistory.Add(new(ChatRole.User, "Hi, I'm looking for book suggestions"));
80113
this.OutputLastMessage(chatHistory);
81114

82-
await foreach (var chatUpdate in chatService.GetStreamingChatMessageContentsAsync(chatHistory))
115+
await foreach (var chatUpdate in ollamaClient.GetStreamingResponseAsync(chatHistory))
83116
{
84-
var innerContent = chatUpdate.InnerContent as ChatResponseStream;
85-
OutputInnerContent(innerContent!);
117+
var rawRepresentation = chatUpdate.RawRepresentation as OllamaSharp.Models.Chat.ChatResponseStream;
118+
OutputOllamaSharpContent(rawRepresentation!);
86119
}
87120
}
88121

89122
/// <summary>
90123
/// Demonstrates how you can template a chat history call while using the <see cref="Kernel"/> for invocation.
91124
/// </summary>
92125
[Fact]
93-
public async Task UsingKernelChatPromptStreamingWithOllama()
126+
public async Task UsingKernelChatPromptStreaming()
94127
{
95128
Assert.NotNull(TestConfiguration.Ollama.ModelId);
96129

97-
Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingKernelChatPromptStreamingWithOllama)} ========");
130+
Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingKernelChatPromptStreaming)} ========");
98131

99132
StringBuilder chatPrompt = new("""
100133
<message role="system">You are a librarian, expert about books</message>
101134
<message role="user">Hi, I'm looking for book suggestions</message>
102135
""");
103136

104137
var kernel = Kernel.CreateBuilder()
105-
.AddOllamaChatCompletion(
138+
.AddOllamaChatClient(
106139
endpoint: new Uri(TestConfiguration.Ollama.Endpoint),
107140
modelId: TestConfiguration.Ollama.ModelId)
108141
.Build();
@@ -124,19 +157,19 @@ public async Task UsingKernelChatPromptStreamingWithOllama()
124157
/// This is a breaking glass scenario and is more susceptible to break on newer versions of OllamaSharp library.
125158
/// </remarks>
126159
[Fact]
127-
public async Task UsingKernelChatPromptStreamingInnerContentsWithOllama()
160+
public async Task UsingKernelChatPromptStreamingRawRepresentation()
128161
{
129162
Assert.NotNull(TestConfiguration.Ollama.ModelId);
130163

131-
Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingKernelChatPromptStreamingInnerContentsWithOllama)} ========");
164+
Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingKernelChatPromptStreamingRawRepresentation)} ========");
132165

133166
StringBuilder chatPrompt = new("""
134167
<message role="system">You are a librarian, expert about books</message>
135168
<message role="user">Hi, I'm looking for book suggestions</message>
136169
""");
137170

138171
var kernel = Kernel.CreateBuilder()
139-
.AddOllamaChatCompletion(
172+
.AddOllamaChatClient(
140173
endpoint: new Uri(TestConfiguration.Ollama.Endpoint),
141174
modelId: TestConfiguration.Ollama.ModelId)
142175
.Build();
@@ -148,8 +181,8 @@ public async Task UsingKernelChatPromptStreamingInnerContentsWithOllama()
148181

149182
await foreach (var chatUpdate in kernel.InvokePromptStreamingAsync<StreamingChatMessageContent>(chatPrompt.ToString()))
150183
{
151-
var innerContent = chatUpdate.InnerContent as ChatResponseStream;
152-
OutputInnerContent(innerContent!);
184+
var innerContent = chatUpdate.InnerContent as OllamaSharp.Models.Chat.ChatResponseStream;
185+
OutputOllamaSharpContent(innerContent!);
153186
}
154187
}
155188

@@ -159,11 +192,11 @@ public async Task UsingKernelChatPromptStreamingInnerContentsWithOllama()
159192
/// and alternatively via the StreamingChatMessageContent.Items property.
160193
/// </summary>
161194
[Fact]
162-
public async Task UsingStreamingTextFromChatCompletionWithOllama()
195+
public async Task UsingStreamingTextFromChatCompletion()
163196
{
164197
Assert.NotNull(TestConfiguration.Ollama.ModelId);
165198

166-
Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingStreamingTextFromChatCompletionWithOllama)} ========");
199+
Console.WriteLine($"======== Ollama - Chat Completion - {nameof(UsingStreamingTextFromChatCompletion)} ========");
167200

168201
using var ollamaClient = new OllamaApiClient(
169202
uriString: TestConfiguration.Ollama.Endpoint,
@@ -212,6 +245,29 @@ private async Task<string> StreamMessageOutputFromKernelAsync(Kernel kernel, str
212245
return fullMessage;
213246
}
214247

248+
private async Task StreamChatClientMessageOutputAsync(IChatClient chatClient, List<ChatMessage> chatHistory)
249+
{
250+
bool roleWritten = false;
251+
string fullMessage = string.Empty;
252+
List<ChatResponseUpdate> chatUpdates = [];
253+
await foreach (var chatUpdate in chatClient.GetStreamingResponseAsync(chatHistory))
254+
{
255+
chatUpdates.Add(chatUpdate);
256+
if (!roleWritten && !string.IsNullOrEmpty(chatUpdate.Text))
257+
{
258+
Console.Write($"Assistant: {chatUpdate.Text}");
259+
roleWritten = true;
260+
}
261+
else if (!string.IsNullOrEmpty(chatUpdate.Text))
262+
{
263+
Console.Write(chatUpdate.Text);
264+
}
265+
}
266+
267+
Console.WriteLine("\n------------------------");
268+
chatHistory.AddRange(chatUpdates.ToChatResponse().Messages);
269+
}
270+
215271
/// <summary>
216272
/// Retrieve extra information from each streaming chunk response.
217273
/// </summary>
@@ -220,7 +276,7 @@ private async Task<string> StreamMessageOutputFromKernelAsync(Kernel kernel, str
220276
/// This is a breaking glass scenario, any attempt on running with different versions of OllamaSharp library that introduces breaking changes
221277
/// may cause breaking changes in the code below.
222278
/// </remarks>
223-
private void OutputInnerContent(ChatResponseStream streamChunk)
279+
private void OutputOllamaSharpContent(OllamaSharp.Models.Chat.ChatResponseStream streamChunk)
224280
{
225281
Console.WriteLine($$"""
226282
Model: {{streamChunk.Model}}
@@ -230,8 +286,8 @@ private void OutputInnerContent(ChatResponseStream streamChunk)
230286
Done: {{streamChunk.Done}}
231287
""");
232288

233-
/// The last message in the chunk is a <see cref="ChatDoneResponseStream"/> type with additional metadata.
234-
if (streamChunk is ChatDoneResponseStream doneStream)
289+
/// The last message in the chunk is a <see cref="OllamaSharp.Models.Chat.ChatDoneResponseStream"/> type with additional metadata.
290+
if (streamChunk is OllamaSharp.Models.Chat.ChatDoneResponseStream doneStream)
235291
{
236292
Console.WriteLine($$"""
237293
Done Reason: {{doneStream.DoneReason}}
@@ -245,4 +301,10 @@ private void OutputInnerContent(ChatResponseStream streamChunk)
245301
}
246302
Console.WriteLine("------------------------");
247303
}
304+
305+
private void OutputLastMessage(List<ChatMessage> chatHistory)
306+
{
307+
var message = chatHistory.Last();
308+
Console.WriteLine($"{message.Role}: {message.Text}");
309+
}
248310
}

0 commit comments

Comments
 (0)