Skip to content

Commit 6d27f97

Browse files
authored
Merge pull request #136 from awaescher/merge-129
Merge 129
2 parents 1ccce48 + 48e1ce7 commit 6d27f97

10 files changed

+155
-63
lines changed

demo/OllamaApiConsole.csproj

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<PropertyGroup>
4-
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
6-
<ImplicitUsings>enable</ImplicitUsings>
7-
<Nullable>enable</Nullable>
8-
</PropertyGroup>
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<NoWarn>IDE0065;IDE0055;IDE0011</NoWarn>
9+
</PropertyGroup>
910

10-
<ItemGroup>
11+
<ItemGroup>
1112
<!--
1213
SixLabors.ImageSharp added explicitly to fix CVE-2024-41131: https://github.com/advisories/GHSA-63p8-c4ww-9cg7
1314
and can be removed once Spectre.Console.ImageSharp uses a version greater than 3.1.4
1415
-->
1516
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
1617
<PackageReference Include="Spectre.Console" Version="0.49.1" />
1718
<PackageReference Include="Spectre.Console.ImageSharp" Version="0.49.1" />
18-
</ItemGroup>
19+
</ItemGroup>
1920

20-
<ItemGroup>
21-
<ProjectReference Include="..\src\OllamaSharp.csproj" />
22-
</ItemGroup>
21+
<ItemGroup>
22+
<ProjectReference Include="..\src\OllamaSharp.csproj" />
23+
</ItemGroup>
2324

2425
</Project>

src/MicrosoftAi/AbstractionMapper.cs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Text.Json;
45
using System.Text.Json.Nodes;
56
using Microsoft.Extensions.AI;
67
using OllamaSharp.Models;
@@ -44,13 +45,14 @@ public static class AbstractionMapper
4445
/// <param name="chatMessages">A list of chat messages.</param>
4546
/// <param name="options">Optional chat options to configure the request.</param>
4647
/// <param name="stream">Indicates if the request should be streamed.</param>
47-
public static ChatRequest ToOllamaSharpChatRequest(IList<ChatMessage> chatMessages, ChatOptions? options, bool stream)
48+
/// <param name="serializerOptions">Serializer options</param>
49+
public static ChatRequest ToOllamaSharpChatRequest(IList<ChatMessage> chatMessages, ChatOptions? options, bool stream, JsonSerializerOptions serializerOptions)
4850
{
4951
var request = new ChatRequest
5052
{
5153
Format = options?.ResponseFormat == ChatResponseFormat.Json ? "json" : null,
5254
KeepAlive = null,
53-
Messages = ToOllamaSharpMessages(chatMessages),
55+
Messages = ToOllamaSharpMessages(chatMessages, serializerOptions),
5456
Model = options?.ModelId ?? "", // will be set OllamaApiClient.SelectedModel if not set
5557
Options = new Models.RequestOptions
5658
{
@@ -190,17 +192,37 @@ private static string ToFunctionTypeString(JsonObject? schema)
190192
/// Converts a list of Microsoft.Extensions.AI.<see cref="ChatMessage"/> to a list of Ollama <see cref="Message"/>.
191193
/// </summary>
192194
/// <param name="chatMessages">The chat messages to convert.</param>
193-
private static IEnumerable<Message> ToOllamaSharpMessages(IList<ChatMessage> chatMessages)
195+
/// <param name="serializerOptions">Serializer options</param>
196+
private static IEnumerable<Message> ToOllamaSharpMessages(IList<ChatMessage> chatMessages, JsonSerializerOptions serializerOptions)
194197
{
195198
foreach (var cm in chatMessages)
196199
{
200+
var images = cm.Contents.OfType<ImageContent>().Select(ToOllamaImage).Where(s => !string.IsNullOrEmpty(s)).ToArray();
201+
var toolCalls = cm.Contents.OfType<FunctionCallContent>().Select(ToOllamaSharpToolCall).ToArray();
202+
197203
yield return new Message
198204
{
199205
Content = cm.Text,
200-
Images = cm.Contents.OfType<ImageContent>().Select(ToOllamaImage).Where(s => !string.IsNullOrEmpty(s)).ToArray(),
206+
Images = images.Length > 0 ? images : null,
201207
Role = ToOllamaSharpRole(cm.Role),
202-
ToolCalls = cm.Contents.OfType<FunctionCallContent>().Select(ToOllamaSharpToolCall),
208+
ToolCalls = toolCalls.Length > 0 ? toolCalls : null,
203209
};
210+
211+
// If the message contains a function result, add it as a separate tool message
212+
foreach (var frc in cm.Contents.OfType<FunctionResultContent>())
213+
{
214+
var jsonResult = JsonSerializer.SerializeToElement(frc.Result, serializerOptions);
215+
216+
yield return new Message
217+
{
218+
Content = JsonSerializer.Serialize(new OllamaFunctionResultContent
219+
{
220+
CallId = frc.CallId,
221+
Result = jsonResult,
222+
}, serializerOptions),
223+
Role = Models.Chat.ChatRole.Tool,
224+
};
225+
}
204226
}
205227
}
206228

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace OllamaSharp.MicrosoftAi;
2+
3+
using System.Text.Json;
4+
5+
internal sealed class OllamaFunctionResultContent
6+
{
7+
public string? CallId { get; set; }
8+
public JsonElement Result { get; set; }
9+
}

src/OllamaApiClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,15 +385,15 @@ private async Task EnsureSuccessStatusCodeAsync(HttpResponseMessage response)
385385
/// <inheritdoc/>
386386
async Task<ChatCompletion> IChatClient.CompleteAsync(IList<ChatMessage> chatMessages, ChatOptions? options, CancellationToken cancellationToken)
387387
{
388-
var request = MicrosoftAi.AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, options, stream: false);
388+
var request = MicrosoftAi.AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, options, stream: false, OutgoingJsonSerializerOptions);
389389
var response = await ChatAsync(request, cancellationToken).StreamToEndAsync().ConfigureAwait(false);
390390
return MicrosoftAi.AbstractionMapper.ToChatCompletion(response, response?.Model ?? request.Model ?? SelectedModel) ?? new ChatCompletion([]);
391391
}
392392

393393
/// <inheritdoc/>
394394
async IAsyncEnumerable<StreamingChatCompletionUpdate> IChatClient.CompleteStreamingAsync(IList<ChatMessage> chatMessages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
395395
{
396-
var request = MicrosoftAi.AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, options, stream: true);
396+
var request = MicrosoftAi.AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, options, stream: true, OutgoingJsonSerializerOptions);
397397
await foreach (var response in ChatAsync(request, cancellationToken).ConfigureAwait(false))
398398
yield return MicrosoftAi.AbstractionMapper.ToStreamingChatCompletionUpdate(response);
399399
}

src/OllamaSharp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<PackageLicenseFile>LICENSE</PackageLicenseFile>
2020
<SignAssembly>True</SignAssembly>
2121
<AssemblyOriginatorKeyFile>..\OllamaSharp.snk</AssemblyOriginatorKeyFile>
22+
<NoWarn>IDE0065;IDE0055;IDE0011;S3881</NoWarn>
2223
</PropertyGroup>
2324

2425
<ItemGroup>

test/AbstractionMapperTests.cs

Lines changed: 81 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Text.Json;
12
using FluentAssertions;
23
using Microsoft.Extensions.AI;
34
using NUnit.Framework;
@@ -8,12 +9,9 @@
89

910
namespace Tests;
1011

11-
#pragma warning disable CS8602 // Dereference of a possibly null reference.
12-
#pragma warning disable CS8604 // Possible null reference argument.
13-
14-
public partial class AbstractionMapperTests
12+
public class AbstractionMapperTests
1513
{
16-
public partial class ToOllamaSharpChatRequestMethod : AbstractionMapperTests
14+
public class ToOllamaSharpChatRequestMethod : AbstractionMapperTests
1715
{
1816
[Test]
1917
public void Maps_Partial_Options_Class()
@@ -26,7 +24,7 @@ public void Maps_Partial_Options_Class()
2624

2725
var options = new ChatOptions { Temperature = 0.5f, /* other properties are left out */ };
2826

29-
var request = AbstractionMapper.ToOllamaSharpChatRequest(messages, options, stream: true);
27+
var request = AbstractionMapper.ToOllamaSharpChatRequest(messages, options, stream: true, JsonSerializerOptions.Default);
3028

3129
request.Options.F16kv.Should().BeNull();
3230
request.Options.FrequencyPenalty.Should().BeNull();
@@ -95,7 +93,7 @@ public void Maps_Messages()
9593
},
9694
};
9795

98-
var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, null, stream: true);
96+
var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, null, stream: true, JsonSerializerOptions.Default);
9997

10098
chatRequest.Messages.Should().HaveCount(3);
10199

@@ -148,7 +146,7 @@ public void Maps_Base64_Images()
148146
},
149147
};
150148

151-
var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, null, stream: true);
149+
var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, null, stream: true, JsonSerializerOptions.Default);
152150

153151
chatRequest.Messages.Should().HaveCount(2);
154152

@@ -180,7 +178,7 @@ public void Maps_Byte_Array_Images()
180178
}
181179
};
182180

183-
var request = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, null, stream: true);
181+
var request = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, null, stream: true, JsonSerializerOptions.Default);
184182
request.Messages.Single().Images.Single().Should().Be("QUJD");
185183
}
186184

@@ -207,7 +205,7 @@ public void Does_Not_Support_Image_Links()
207205

208206
Action act = () =>
209207
{
210-
var request = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, null, stream: true);
208+
var request = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, null, stream: true, JsonSerializerOptions.Default);
211209
request.Messages.Should().NotBeEmpty(); // access .Messages to invoke the evaluation of IEnumerable<Message>
212210
};
213211

@@ -234,7 +232,7 @@ public void Maps_Messages_With_Tools()
234232
Tools = [new WeatherFunction()]
235233
};
236234

237-
var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, options, stream: true);
235+
var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, options, stream: true, JsonSerializerOptions.Default);
238236

239237
var tool = chatRequest.Tools.Single();
240238
tool.Function.Description.Should().Be("Gets the current weather for a current location");
@@ -251,6 +249,73 @@ public void Maps_Messages_With_Tools()
251249
tool.Type.Should().Be("function");
252250
}
253251

252+
[Test]
253+
public void Maps_Messages_With_ToolResponse()
254+
{
255+
var chatMessages = new List<Microsoft.Extensions.AI.ChatMessage>
256+
{
257+
new()
258+
{
259+
AdditionalProperties = [],
260+
AuthorName = "a1",
261+
RawRepresentation = null,
262+
Role = Microsoft.Extensions.AI.ChatRole.Tool,
263+
Text = "The weather in Honolulu is 25°C."
264+
}
265+
};
266+
267+
var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, new(), stream: true, JsonSerializerOptions.Default);
268+
269+
var tool = chatRequest.Messages.Single();
270+
tool.Content.Should().Contain("The weather in Honolulu is 25°C.");
271+
tool.Role.Should().Be(OllamaSharp.Models.Chat.ChatRole.Tool);
272+
}
273+
274+
[Test]
275+
public void Maps_Messages_With_MultipleToolResponse()
276+
{
277+
var aiChatMessages = new List<Microsoft.Extensions.AI.ChatMessage>
278+
{
279+
new()
280+
{
281+
AdditionalProperties = [],
282+
AuthorName = "a1",
283+
RawRepresentation = null,
284+
Role = Microsoft.Extensions.AI.ChatRole.User,
285+
Contents = [
286+
new TextContent("I have found those 2 results"),
287+
new FunctionResultContent(
288+
callId: "123",
289+
name: "Function1",
290+
result: new { Temperature = 40 }),
291+
292+
new FunctionResultContent(
293+
callId: "456",
294+
name: "Function2",
295+
result: new { Summary = "This is a tool result test" }
296+
),
297+
]
298+
}
299+
};
300+
301+
var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(aiChatMessages, new(), stream: true, JsonSerializerOptions.Default);
302+
var chatMessages = chatRequest.Messages?.ToList();
303+
304+
chatMessages.Should().HaveCount(3);
305+
306+
var user = chatMessages[0];
307+
var tool1 = chatMessages[1];
308+
var tool2 = chatMessages[2];
309+
tool1.Content.Should().Contain("\"Temperature\":40");
310+
tool1.Content.Should().Contain("\"CallId\":\"123\"");
311+
tool1.Role.Should().Be(OllamaSharp.Models.Chat.ChatRole.Tool);
312+
tool2.Content.Should().Contain("\"Summary\":\"This is a tool result test\"");
313+
tool2.Content.Should().Contain("\"CallId\":\"456\"");
314+
tool2.Role.Should().Be(OllamaSharp.Models.Chat.ChatRole.Tool);
315+
user.Content.Should().Contain("I have found those 2 results");
316+
user.Role.Should().Be(OllamaSharp.Models.Chat.ChatRole.User);
317+
}
318+
254319
[Test]
255320
public void Maps_Options()
256321
{
@@ -268,7 +333,7 @@ public void Maps_Options()
268333
TopP = 10.1f
269334
};
270335

271-
var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, options, stream: true);
336+
var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(chatMessages, options, stream: true, JsonSerializerOptions.Default);
272337

273338
chatRequest.Format.Should().Be("json");
274339
chatRequest.Model.Should().Be("llama3.1:405b");
@@ -346,7 +411,7 @@ public void Maps_Ollama_Options()
346411
.AddOllamaOption(OllamaOption.UseMmap, true)
347412
.AddOllamaOption(OllamaOption.VocabOnly, false);
348413

349-
var ollamaRequest = AbstractionMapper.ToOllamaSharpChatRequest([], options, stream: true);
414+
var ollamaRequest = AbstractionMapper.ToOllamaSharpChatRequest([], options, stream: true, JsonSerializerOptions.Default);
350415

351416
ollamaRequest.Options.F16kv.Should().Be(true);
352417
ollamaRequest.Options.FrequencyPenalty.Should().Be(0.11f);
@@ -507,7 +572,7 @@ public void Maps_ToolCalls()
507572
}
508573
}
509574

510-
public partial class ToOllamaEmbedRequestMethod : AbstractionMapperTests
575+
public class ToOllamaEmbedRequestMethod : AbstractionMapperTests
511576
{
512577
[Test]
513578
public void Maps_Request()
@@ -544,7 +609,7 @@ public void Maps_KeepAlive_And_Truncate_From_AdditionalProperties()
544609
}
545610
}
546611

547-
public partial class ToGeneratedEmbeddingsMethod : AbstractionMapperTests
612+
public class ToGeneratedEmbeddingsMethod : AbstractionMapperTests
548613
{
549614
[Test]
550615
public void Maps_Response()
@@ -575,7 +640,4 @@ public void Maps_Response()
575640
mappedResponse.Usage.TotalTokenCount.Should().Be(18);
576641
}
577642
}
578-
}
579-
580-
#pragma warning restore CS8602 // Dereference of a possibly null reference.
581-
#pragma warning restore CS8604 // Possible null reference argument.
643+
}

test/ChatTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public async Task Sends_Assistant_ToolsCall_To_Streamer()
6161

6262
chat.Messages.Last().Role.Should().Be(ChatRole.Assistant);
6363
chat.Messages.Last().ToolCalls.Should().HaveCount(1);
64-
chat.Messages.Last().ToolCalls!.ElementAt(0).Function!.Name.Should().Be("get_current_weather");
64+
chat.Messages.Last().ToolCalls.ElementAt(0).Function.Name.Should().Be("get_current_weather");
6565
}
6666

6767
[Test]
@@ -117,7 +117,6 @@ public async Task Sends_Messages_As_Defined_Role()
117117
history[1].Content.Should().Be("Hi tool.");
118118
}
119119

120-
121120
[Test]
122121
public async Task Sends_Image_Bytes_As_Base64()
123122
{

test/IAsyncEnumerableExtensionTests.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
namespace Tests;
88

9-
#pragma warning disable CS8602 // Dereference of a possibly null reference.
10-
119
public class IAsyncEnumerableExtensionTests
1210
{
1311
public class StreamToEndAsyncMethod : IAsyncEnumerableExtensionTests
@@ -73,6 +71,4 @@ public async Task Throws_If_No_Done_Response_Was_Send()
7371
private static Message CreateMessage(ChatRole role, string content)
7472
=> new() { Role = role, Content = content };
7573
}
76-
}
77-
78-
#pragma warning restore CS8602 // Dereference of a possibly null reference.
74+
}

0 commit comments

Comments
 (0)