Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ public static ChatMessageContentItemCollection ToChatMessageContentItemCollectio
KernelArguments? arguments = null;
try
{
arguments = JsonSerializer.Deserialize<KernelArguments>(functionCallResponseItem.FunctionArguments);
if (!string.IsNullOrWhiteSpace(functionCallResponseItem.FunctionArguments?.ToString()))
{
arguments = JsonSerializer.Deserialize<KernelArguments>(functionCallResponseItem.FunctionArguments);
}
}
catch (JsonException ex)
{
Expand Down Expand Up @@ -116,7 +119,10 @@ public static FunctionCallContent ToFunctionCallContent(this FunctionCallRespons
KernelArguments? arguments = null;
try
{
arguments = JsonSerializer.Deserialize<KernelArguments>(functionCallResponseItem.FunctionArguments);
if (!string.IsNullOrWhiteSpace(functionCallResponseItem.FunctionArguments?.ToString()))
{
arguments = JsonSerializer.Deserialize<KernelArguments>(functionCallResponseItem.FunctionArguments);
}
}
catch (JsonException ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,41 @@ public void VerifyToFunctionCallContent()
Assert.NotNull(content.Arguments);
}

/// <summary>
/// Verify that a function call without arguments, as some OpenAI compatible servers return for a function without parameters, is not reported as invalid JSON.
/// </summary>
[Fact]
public void VerifyToFunctionCallContentWithEmptyArguments()
{
// Arrange
FunctionCallResponseItem responseItem = FunctionCallResponseItem.CreateFunctionCallItem("callId", "functionName", new BinaryData(string.Empty));

// Act
FunctionCallContent content = responseItem.ToFunctionCallContent();

// Assert
Assert.Null(content.Exception);
Assert.Null(content.Arguments);
}

/// <summary>
/// Verify that a function call without arguments is not reported as invalid JSON when converted to a <see cref="ChatMessageContentItemCollection"/>.
/// </summary>
[Fact]
public void VerifyToChatMessageContentItemCollectionWithEmptyFunctionCallArguments()
{
// Arrange
FunctionCallResponseItem responseItem = FunctionCallResponseItem.CreateFunctionCallItem("callId", "functionName", new BinaryData(string.Empty));

// Act
ChatMessageContentItemCollection collection = responseItem.ToChatMessageContentItemCollection();

// Assert
FunctionCallContent content = Assert.IsType<FunctionCallContent>(Assert.Single(collection));
Assert.Null(content.Exception);
Assert.Null(content.Arguments);
}

/// <summary>
/// Verify that ReasoningResponseItem with SummaryParts generates ReasoningContent correctly.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.MistralAI;
using Microsoft.SemanticKernel.Connectors.MistralAI.Client;
using Xunit;

namespace SemanticKernel.Connectors.MistralAI.UnitTests;

/// <summary>
/// Unit tests for <see cref="MistralAIPluginCollectionExtensions"/>.
/// </summary>
public sealed class MistralAIPluginCollectionExtensionsTests
{
[Theory]
[InlineData("")]
[InlineData(" ")]
public void ItTreatsEmptyFunctionArgumentsAsNoArguments(string arguments)
{
// Arrange
var kernel = new Kernel();
kernel.ImportPluginFromFunctions("MyPlugin", [KernelFunctionFactory.CreateFromMethod(() => "noon", "GetTime")]);
var toolCallFunction = new MistralFunction("MyPlugin-GetTime", string.Empty, null) { Arguments = arguments };

// Act
bool found = kernel.Plugins.TryGetFunctionAndArguments(toolCallFunction, out KernelFunction? function, out KernelArguments? kernelArguments);

// Assert
Assert.True(found);
Assert.NotNull(function);
Assert.Null(kernelArguments);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,7 @@ private void AddFunctionCallContent(ChatMessageContent message, MistralToolCall
// This allows consumers to work with functions in an LLM-agnostic way.
Exception? exception = null;
KernelArguments? arguments = null;
if (toolCall.Function.Arguments is not null)
if (!string.IsNullOrWhiteSpace(toolCall.Function.Arguments))
{
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal static bool TryGetFunctionAndArguments(
{
// Add parameters to arguments
arguments = null;
if (functionToolCall.Arguments is not null)
if (!string.IsNullOrWhiteSpace(functionToolCall.Arguments))
{
// TODO user serializer options from the Kernel
var functionArguments = JsonSerializer.Deserialize<Dictionary<string, object>>(functionToolCall.Arguments);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Xunit;

namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services;

/// <summary>
/// Tests for tool calls that come back without arguments from OpenAI compatible servers.
/// </summary>
public sealed class OpenAIChatCompletionEmptyToolArgumentsTests : IDisposable
{
private readonly HttpMessageHandlerStub _messageHandlerStub;
private readonly HttpClient _httpClient;

public OpenAIChatCompletionEmptyToolArgumentsTests()
{
this._messageHandlerStub = new HttpMessageHandlerStub();
this._httpClient = new HttpClient(this._messageHandlerStub, false);
}

[Theory]
[InlineData("")]
[InlineData(" ")]
public async Task ItDoesNotReportAToolCallWithoutArgumentsAsInvalidJsonAsync(string arguments)
{
// Arrange
string response = $$"""
{
"id": "response-id",
"object": "chat.completion",
"created": 1699896916,
"model": "gpt-4o",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{ "id": "1", "type": "function", "function": { "name": "MyPlugin-GetTime", "arguments": "{{arguments}}" } }
]
},
"finish_reason": "tool_calls"
}
],
"usage": { "prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15 }
}
""";
using var httpResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(response, Encoding.UTF8, "application/json") };
this._messageHandlerStub.ResponseToReturn = httpResponse;

var kernel = new Kernel();
kernel.ImportPluginFromFunctions("MyPlugin", [KernelFunctionFactory.CreateFromMethod(() => "noon", "GetTime")]);
var sut = new OpenAIChatCompletionService(modelId: "gpt-4o", apiKey: "NOKEY", httpClient: this._httpClient);

// Act
var result = await sut.GetChatMessageContentAsync(
new ChatHistory("What time is it?"),
new OpenAIPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false) },
kernel);

// Assert
var functionCall = Assert.Single(result.Items.OfType<FunctionCallContent>());
Assert.Null(functionCall.Exception);
Assert.Null(functionCall.Arguments);
Assert.Equal("GetTime", functionCall.FunctionName);
}

public void Dispose()
{
this._httpClient.Dispose();
this._messageHandlerStub.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,11 @@ private List<FunctionCallContent> GetFunctionCallContents(IEnumerable<ChatToolCa
KernelArguments? arguments = null;
try
{
arguments = JsonSerializer.Deserialize<KernelArguments>(toolCall.FunctionArguments);
if (!string.IsNullOrWhiteSpace(toolCall.FunctionArguments?.ToString()))
{
arguments = JsonSerializer.Deserialize<KernelArguments>(toolCall.FunctionArguments!);
}

if (arguments is { Count: > 0 } && !retainArgumentTypes)
{
// Iterate over copy of the names to avoid mutating the dictionary while enumerating it
Expand Down
Loading