Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.3" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.3.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
Expand Down
54 changes: 24 additions & 30 deletions src/OllamaSharp/MicrosoftAi/AbstractionMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using OllamaSharp.Constants;
using OllamaSharp.Models;
using OllamaSharp.Models.Chat;

using ChatRole = OllamaSharp.Models.Chat.ChatRole;

namespace OllamaSharp.MicrosoftAi;
Expand Down Expand Up @@ -80,6 +81,18 @@ public static ChatRequest ToOllamaSharpChatRequest(IChatClient? chatClient, IEnu
request.Options.TopK ??= options?.TopK;
request.Options.NumPredict = options?.MaxOutputTokens;

if (options?.Reasoning is { } reasoning)
{
request.Think ??= reasoning.Effort switch
{
null => (ThinkValue?)null,
ReasoningEffort.None => false,
ReasoningEffort.Low => new ThinkValue(ThinkValue.Low),
ReasoningEffort.Medium => new ThinkValue(ThinkValue.Medium),
_ => new ThinkValue(ThinkValue.High),
};
}

request.Messages =
request.Messages is null ? mappedMessages :
mappedMessages is null ? request.Messages :
Expand Down Expand Up @@ -176,45 +189,26 @@ private static void TryAddOption<T>(ChatOptions? microsoftChatOptions, string op
}

/// <summary>
/// Converts an <see cref="AIFunctionDeclaration"/> to a <see cref="Tool"/>.
/// Converts an <see cref="AIFunctionDeclaration"/> to a JSON tool representation
/// compatible with the Ollama API.
/// </summary>
/// <param name="function">The function to convert.</param>
/// <returns>A <see cref="Tool"/> object containing the converted data.</returns>
private static Tool ToOllamaSharpTool(AIFunctionDeclaration function)
/// <returns>A <see cref="JsonNode"/> representing the tool in Ollama's expected format.</returns>
private static JsonNode? ToOllamaSharpTool(AIFunctionDeclaration function)
{
JsonElement transformedSchema = _schemaTransformCache.GetOrCreateTransformedSchema(function);
return new Tool
return new JsonObject
{
Function = new Function
["type"] = Application.Function,
[Application.Function] = new JsonObject
{
Description = function.Description,
Name = function.Name,
Parameters = JsonSerializer.Deserialize<Parameters>(transformedSchema),
},
Type = Application.Function
[Application.Name] = function.Name,
["description"] = function.Description,
[Application.Parameters] = JsonNode.Parse(transformedSchema.GetRawText()),
}
};
}

/// <summary>
/// Converts parameter schema object to a function type string.
/// </summary>
/// <param name="schema">The schema object holding schema type information.</param>
/// <returns>A collection of strings containing the function types.</returns>
private static IEnumerable<string> GetPossibleValues(JsonObject? schema)
{
return []; // TODO others supported?
}

/// <summary>
/// Converts parameter schema object to a function type string.
/// </summary>
/// <param name="schema">The schema object holding schema type information.</param>
/// <returns>A string containing the function type.</returns>
private static string ToFunctionTypeString(JsonObject? schema)
{
return "string"; // TODO others supported?
}

/// <summary>
/// Converts a list of Microsoft.Extensions.AI.<see cref="ChatMessage"/> to a list of Ollama <see cref="Message"/>.
/// </summary>
Expand Down
127 changes: 114 additions & 13 deletions test/AbstractionMapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,19 +252,52 @@ public void Maps_Messages_With_Tools()

var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(null, chatMessages, options, stream: true, JsonSerializerOptions.Default);

var tool = (Tool)chatRequest.Tools.Single();
tool.Function.Description.ShouldBe("Gets the current weather for a current location");
tool.Function.Name.ShouldBe("get_weather");
tool.Function.Parameters.Properties.Count.ShouldBe(2);
tool.Function.Parameters.Properties["city"].Description.ShouldBe("The city to get the weather for");
tool.Function.Parameters.Properties["city"].Enum.ShouldBeNull();
tool.Function.Parameters.Properties["city"].Type.ShouldBe("string");
tool.Function.Parameters.Properties["unit"].Description.ShouldBe("The unit to calculate the current temperature to");
tool.Function.Parameters.Properties["unit"].Enum.ShouldBeNull();
tool.Function.Parameters.Properties["unit"].Type.ShouldBe("string");
tool.Function.Parameters.Required.ShouldBe(["city"], ignoreOrder: true);
tool.Function.Parameters.Type.ShouldBe("object");
tool.Type.ShouldBe("function");
var toolJson = JsonSerializer.SerializeToElement(chatRequest.Tools.Single());
toolJson.GetProperty("type").GetString().ShouldBe("function");
toolJson.GetProperty("function").GetProperty("description").GetString().ShouldBe("Gets the current weather for a current location");
toolJson.GetProperty("function").GetProperty("name").GetString().ShouldBe("get_weather");

var parameters = toolJson.GetProperty("function").GetProperty("parameters");
parameters.GetProperty("type").GetString().ShouldBe("object");
parameters.GetProperty("required").EnumerateArray().Select(e => e.GetString()).ShouldBe(["city"], ignoreOrder: true);

var properties = parameters.GetProperty("properties");
properties.GetProperty("city").GetProperty("description").GetString().ShouldBe("The city to get the weather for");
properties.GetProperty("city").GetProperty("type").GetString().ShouldBe("string");
properties.GetProperty("unit").GetProperty("description").GetString().ShouldBe("The unit to calculate the current temperature to");
properties.GetProperty("unit").GetProperty("type").GetString().ShouldBe("string");
}

/// <summary>
/// Verifies that tools with nullable parameters (where JSON schema type is an array) are handled correctly.
/// </summary>
[Test]
public void Maps_Messages_With_Tools_Nullable_Parameters()
{
var chatMessages = new List<Microsoft.Extensions.AI.ChatMessage>
{
new(Microsoft.Extensions.AI.ChatRole.User, "Filter files")
};

var options = new ChatOptions
{
Tools = [AIFunctionFactory.Create((
[System.ComponentModel.Description("The filter to apply")] string? filenameFilter) => "done",
"filter_files", "Filters files by name")],
};

// This should not throw - previously it crashed when Property.Type was an array like ["string", "null"]
var chatRequest = AbstractionMapper.ToOllamaSharpChatRequest(null, chatMessages, options, stream: true, JsonSerializerOptions.Default);

var toolJson = JsonSerializer.SerializeToElement(chatRequest.Tools.Single());
toolJson.GetProperty("type").GetString().ShouldBe("function");
toolJson.GetProperty("function").GetProperty("name").GetString().ShouldBe("filter_files");

var parameters = toolJson.GetProperty("function").GetProperty("parameters");
parameters.GetProperty("type").GetString().ShouldBe("object");

var filenameFilter = parameters.GetProperty("properties").GetProperty("filenameFilter");
filenameFilter.GetProperty("description").GetString().ShouldBe("The filter to apply");
}

/// <summary>
Expand Down Expand Up @@ -779,6 +812,74 @@ public void Maps_UsesRawRepresentation()
ollamaRequest.Options.VocabOnly.Value.ShouldBeTrue();
}

/// <summary>
/// Verifies that ReasoningOptions.Output=None without Effort leaves Think as null.
/// </summary>
[Test]
public void Maps_Reasoning_Output_None_Disables_Think()
{
var options = new ChatOptions
{
Reasoning = new ReasoningOptions { Output = ReasoningOutput.None }
};

var request = AbstractionMapper.ToOllamaSharpChatRequest(null, [], options, stream: true, JsonSerializerOptions.Default);

request.Think.ShouldBeNull();
}

/// <summary>
/// Verifies that ReasoningOptions with effort maps to Ollama think levels.
/// </summary>
[TestCase(ReasoningEffort.Low, "low")]
[TestCase(ReasoningEffort.Medium, "medium")]
[TestCase(ReasoningEffort.High, "high")]
[TestCase(ReasoningEffort.ExtraHigh, "high")]
public void Maps_Reasoning_Effort_To_Think_Level(ReasoningEffort effort, string expectedThink)
{
var options = new ChatOptions
{
Reasoning = new ReasoningOptions { Effort = effort }
};

var request = AbstractionMapper.ToOllamaSharpChatRequest(null, [], options, stream: true, JsonSerializerOptions.Default);

request.Think.ShouldBe(expectedThink);
}

/// <summary>
/// Verifies that ReasoningOptions with no specific effort leaves Think as null.
/// </summary>
[Test]
public void Maps_Reasoning_Without_Effort_Enables_Think()
{
var options = new ChatOptions
{
Reasoning = new ReasoningOptions { Output = ReasoningOutput.Full }
};

var request = AbstractionMapper.ToOllamaSharpChatRequest(null, [], options, stream: true, JsonSerializerOptions.Default);

request.Think.ShouldBeNull();
}

/// <summary>
/// Verifies that Reasoning does not override a value set via RawRepresentationFactory.
/// </summary>
[Test]
public void Maps_Reasoning_Does_Not_Override_RawRepresentation()
{
var options = new ChatOptions
{
Reasoning = new ReasoningOptions { Effort = ReasoningEffort.High },
RawRepresentationFactory = _ => new ChatRequest { Think = false },
};

var request = AbstractionMapper.ToOllamaSharpChatRequest(new MockChatClient(), [], options, stream: true, JsonSerializerOptions.Default);

request.Think.ShouldBe(false);
}

private sealed class MockChatClient : IChatClient
{
public void Dispose() { }
Expand Down
Loading