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 @@ -77,7 +77,7 @@ public IReadOnlyList<FunctionCallContent> Build()

if (this._functionNamesByIndex?.TryGetValue(functionCallIndexAndId.Key, out string? fqn) ?? false)
{
var functionFullyQualifiedName = Microsoft.SemanticKernel.FunctionName.Parse(fqn);
var functionFullyQualifiedName = ParseFullyQualifiedFunctionName(fqn);
pluginName = functionFullyQualifiedName.PluginName;
functionName = functionFullyQualifiedName.Name;
}
Expand Down Expand Up @@ -170,6 +170,28 @@ public IReadOnlyList<FunctionCallContent> Build()
/// <param name="functionCallIdsByIndex">The dictionary of function call IDs by function call index.</param>
/// <param name="functionNamesByIndex">The dictionary of function names by function call index.</param>
/// <param name="functionArgumentBuildersByIndex">The dictionary of function argument builders by function call index.</param>

/// <summary>
/// Parses a fully-qualified function name into plugin and function parts.
/// Tries multiple separators (".", "_", "-") since different AI connectors use different formats.
/// </summary>
private static FunctionName ParseFullyQualifiedFunctionName(string fullyQualifiedName)
{
foreach (var separator in new[] { ".", "_", "-" })
{
if (fullyQualifiedName.Contains(separator, StringComparison.Ordinal))
{
var parsed = FunctionName.Parse(fullyQualifiedName, separator);
Comment on lines +180 to +184

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: This separator-guessing loop will falsely split any bare function name containing ., _, or -. For example, FunctionName.Parse("get_weather", "_") produces PluginName="get", Name="weather", and the null check on line 185 passes because a non-null PluginName is always produced when the separator exists. Names with multiple underscores (e.g., my_tools_v2_ReadFile) split incorrectly as plugin=my, function=tools_v2_ReadFile. The separator must come from the connector that formatted the name, not be guessed from the string.

Suggested change
foreach (var separator in new[] { ".", "_", "-" })
{
if (fullyQualifiedName.Contains(separator, StringComparison.Ordinal))
{
var parsed = FunctionName.Parse(fullyQualifiedName, separator);
// TODO: accept the separator used by the originating connector instead of guessing.
private static FunctionName ParseFullyQualifiedFunctionName(string fullyQualifiedName)
{
return FunctionName.Parse(fullyQualifiedName);
}

if (parsed.PluginName is not null)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: The parsed.PluginName is not null check is insufficient—any name containing the separator produces a non-null PluginName. An adversarial model response like admin_DeleteAllData would be parsed as plugin=admin, function=DeleteAllData, potentially routing to an unintended privileged function.

{
return parsed;
}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including - as a candidate separator will silently mis-parse any hyphenated function name as a plugin-qualified call. Hyphens are far more common in identifiers than as SK plugin separators and should not be probed here.

return FunctionName.Parse(fullyQualifiedName);
}

Comment on lines +173 to +194

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structural issue: inserting the new method here splits the XML <param> doc comments (lines 170-172) from their target method TrackStreamingFunctionCallUpdate (now at line 195), orphaning the parameter documentation. Move this method above the doc block or below TrackStreamingFunctionCallUpdate.

private static void TrackStreamingFunctionCallUpdate(StreamingFunctionCallUpdateContent update, ref Dictionary<string, string>? functionCallIdsByIndex, ref Dictionary<string, string>? functionNamesByIndex, ref Dictionary<string, StringBuilder>? functionArgumentBuildersByIndex)
{
if (update is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,48 @@ public void ItShouldCaptureArgumentsDeserializationException(JsonSerializerOptio
Assert.NotNull(functionCall.Exception);
}

[Fact]
public void ItShouldParseUnderscoreSeparatorForOllamaAndGeminiConnectors()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test name time_ReadFile is unambiguous because time is clearly a plugin. Add a test for a bare function name containing underscores (e.g., read_file with no plugin) to verify it is NOT incorrectly split into plugin=read, name=file.

{
// Arrange - Ollama/Gemini use underscore (e.g. time_ReadFile) per FullyQualifiedAIFunction
var sut = new FunctionCallContentBuilder();

// Act
var update1 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 1, functionCallIndex: 0, callId: "call_1", name: "time_ReadFile", arguments: null);
sut.Append(update1);

var update2 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 1, functionCallIndex: 0, callId: null, name: null, arguments: "{\"filePath\":\"d:/test.txt\"}");
sut.Append(update2);

var functionCalls = sut.Build();

// Assert
var functionCall = Assert.Single(functionCalls);
Assert.Equal("call_1", functionCall.Id);
Assert.Equal("time", functionCall.PluginName);
Assert.Equal("ReadFile", functionCall.FunctionName);
Assert.NotNull(functionCall.Arguments);
Assert.Equal("d:/test.txt", functionCall.Arguments["filePath"]);
}

[Fact]
public void ItShouldParseDotSeparatorForFunctionChoiceBehavior()
{
// Arrange - FunctionChoiceBehavior uses dot (e.g. time.ReadFile)
var sut = new FunctionCallContentBuilder();

// Act
var update = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 1, functionCallIndex: 0, callId: "call_1", name: "time.ReadFile", arguments: null);
sut.Append(update);

var functionCalls = sut.Build();

// Assert
var functionCall = Assert.Single(functionCalls);
Assert.Equal("time", functionCall.PluginName);
Assert.Equal("ReadFile", functionCall.FunctionName);
}

private static StreamingChatMessageContent CreateStreamingContentWithFunctionCallUpdate(int choiceIndex, int functionCallIndex, string? callId, string? name, string? arguments, int requestIndex = 0)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage: (1) the hyphen separator path is untested—add a test with e.g. "time-ReadFile" asserting PluginName="time", FunctionName="ReadFile"; (2) the fallback FunctionName.Parse(fullyQualifiedName) path is untested—add a test with a plain name like "ReadFile" asserting PluginName is null and FunctionName is "ReadFile".

{
var content = new StreamingChatMessageContent(AuthorRole.Assistant, null);
Expand Down
Loading