Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Original file line number Diff line number Diff line change
Expand Up @@ -749,11 +749,21 @@ private static string GetFunctionName(MethodInfo method)
string name = SanitizeMemberName(method.Name);

const string AsyncSuffix = "Async";
if (IsAsyncMethod(method) &&
name.EndsWith(AsyncSuffix, StringComparison.Ordinal) &&
name.Length > AsyncSuffix.Length)
if (IsAsyncMethod(method))
{
name = name.Substring(0, name.Length - AsyncSuffix.Length);
// If the method ends in "Async" or contains "Async_", remove the "Async".
int asyncIndex = name.LastIndexOf(AsyncSuffix, StringComparison.Ordinal);
if (asyncIndex > 0 &&
(asyncIndex + AsyncSuffix.Length == name.Length ||
((asyncIndex + AsyncSuffix.Length < name.Length) && (name[asyncIndex + AsyncSuffix.Length] == '_'))))
{
name =
#if NET
string.Concat(name.AsSpan(0, asyncIndex), name.AsSpan(asyncIndex + AsyncSuffix.Length));
#else
string.Concat(name.Substring(0, asyncIndex), name.Substring(asyncIndex + AsyncSuffix.Length));
#endif
}
}

return name;
Expand Down Expand Up @@ -1105,16 +1115,37 @@ private record struct DescriptorKey(
/// Replaces non-alphanumeric characters in the identifier with the underscore character.
/// Primarily intended to remove characters produced by compiler-generated method name mangling.
/// </returns>
private static string SanitizeMemberName(string memberName) =>
InvalidNameCharsRegex().Replace(memberName, "_");
private static string SanitizeMemberName(string memberName)
{
// Handle compiler-generated names (local functions and lambdas)
// Local functions: <ContainingMethod>g__LocalFunctionName|ordinal_depth -> ContainingMethod_LocalFunctionName_ordinal_depth
// Lambdas: <ContainingMethod>b__ordinal_depth -> ContainingMethod_ordinal_depth
if (CompilerGeneratedNameRegex().Match(memberName) is { Success: true } match)
{
memberName = $"{match.Groups[1].Value}_{match.Groups[2].Value}";
}

// Replace all non-alphanumeric characters with underscores.
return InvalidNameCharsRegex().Replace(memberName, "_");
}

/// <summary>Regex that matches compiler-generated names (local functions and lambdas).</summary>
#if NET
[GeneratedRegex(@"^<([^>]+)>\w__(.+)")]
private static partial Regex CompilerGeneratedNameRegex();
#else
private static Regex CompilerGeneratedNameRegex() => _compilerGeneratedNameRegex;
private static readonly Regex _compilerGeneratedNameRegex = new(@"^<([^>]+)>\w__(.+)", RegexOptions.Compiled);
#endif

/// <summary>Regex that flags any character other than ASCII digits or letters or the underscore.</summary>
/// <summary>Regex that flags any character other than ASCII digits or letters.</summary>
/// <remarks>Underscore isn't included so that sequences of underscores are replaced by a single one.</remarks>
#if NET
[GeneratedRegex("[^0-9A-Za-z_]")]
[GeneratedRegex("[^0-9A-Za-z]+")]
private static partial Regex InvalidNameCharsRegex();
#else
private static Regex InvalidNameCharsRegex() => _invalidNameCharsRegex;
private static readonly Regex _invalidNameCharsRegex = new("[^0-9A-Za-z_]", RegexOptions.Compiled);
private static readonly Regex _invalidNameCharsRegex = new("[^0-9A-Za-z]+", RegexOptions.Compiled);
#endif

/// <summary>Invokes the MethodInfo with the specified target object and arguments.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,8 @@ private sealed class MyFunctionTypeWithOneArg(MyArgumentType arg)

private sealed class MyArgumentType;

private static int TestStaticMethod(int a, int b) => a + b;

private class A;
private class B : A;
private sealed class C : B;
Expand Down Expand Up @@ -1039,6 +1041,143 @@ private static AIFunctionFactoryOptions CreateKeyedServicesSupportOptions() =>
},
};

[Fact]
public void LocalFunction_NameCleanup()
{
static void DoSomething()
{
// Empty local function for testing name cleanup
}

var tool = AIFunctionFactory.Create(DoSomething);

// The name should start with: ContainingMethodName_LocalFunctionName (followed by ordinal)
Assert.StartsWith("LocalFunction_NameCleanup_DoSomething_", tool.Name);
}

[Fact]
public void LocalFunction_MultipleInSameMethod()
{
static void FirstLocal()
{
// Empty local function for testing name cleanup
}

static void SecondLocal()
{
// Empty local function for testing name cleanup
}

var tool1 = AIFunctionFactory.Create(FirstLocal);
var tool2 = AIFunctionFactory.Create(SecondLocal);

// Each should have unique names based on the local function name (including ordinal)
Assert.StartsWith("LocalFunction_MultipleInSameMethod_FirstLocal_", tool1.Name);
Assert.StartsWith("LocalFunction_MultipleInSameMethod_SecondLocal_", tool2.Name);
Assert.NotEqual(tool1.Name, tool2.Name);
}

[Fact]
public void Lambda_NameCleanup()
{
Action lambda = () =>
{
// Empty lambda for testing name cleanup
};

var tool = AIFunctionFactory.Create(lambda);

// The name should be the containing method name with ordinal for uniqueness
Assert.StartsWith("Lambda_NameCleanup", tool.Name);
}

[Fact]
public void Lambda_MultipleInSameMethod()
{
Action lambda1 = () =>
{
// Empty lambda for testing name cleanup
};

Action lambda2 = () =>
{
// Empty lambda for testing name cleanup
};

var tool1 = AIFunctionFactory.Create(lambda1);
var tool2 = AIFunctionFactory.Create(lambda2);

// Each lambda should have a unique name based on its ordinal
// to allow the LLM to distinguish between them
Assert.StartsWith("Lambda_MultipleInSameMethod", tool1.Name);
Assert.StartsWith("Lambda_MultipleInSameMethod", tool2.Name);
Assert.NotEqual(tool1.Name, tool2.Name);
}

[Fact]
public void LocalFunction_WithParameters()
{
static int Add(int firstNumber, int secondNumber) => firstNumber + secondNumber;

var tool = AIFunctionFactory.Create(Add);

Assert.StartsWith("LocalFunction_WithParameters_Add_", tool.Name);
Assert.Contains("firstNumber", tool.JsonSchema.ToString());
Assert.Contains("secondNumber", tool.JsonSchema.ToString());
}

[Fact]
public async Task LocalFunction_AsyncFunction()
{
static async Task<string> FetchDataAsync()
{
await Task.Yield();
return "data";
}

var tool = AIFunctionFactory.Create(FetchDataAsync);

// Should strip "Async" suffix and include ordinal
Assert.StartsWith("LocalFunction_AsyncFunction_FetchData_", tool.Name);

var result = await tool.InvokeAsync();
AssertExtensions.EqualFunctionCallResults("data", result);
}

[Fact]
public void LocalFunction_ExplicitNameOverride()
{
static void DoSomething()
{
// Empty local function for testing name cleanup
}

var tool = AIFunctionFactory.Create(DoSomething, name: "CustomName");

Assert.Equal("CustomName", tool.Name);
}

[Fact]
public void LocalFunction_InsideTestMethod()
{
// Even local functions defined in test methods get cleaned up
var tool = AIFunctionFactory.Create(Add, serializerOptions: JsonContext.Default.Options);

Assert.StartsWith("LocalFunction_InsideTestMethod_Add_", tool.Name);

[return: Description("The summed result")]
static int Add(int a, int b) => a + b;
}

[Fact]
public void RegularStaticMethod_NameUnchanged()
{
// Test that actual static methods (not local functions) have names unchanged
var tool = AIFunctionFactory.Create(TestStaticMethod, null, serializerOptions: JsonContext.Default.Options);

Assert.Equal("TestStaticMethod", tool.Name);
}

[JsonSerializable(typeof(IAsyncEnumerable<int>))]
[JsonSerializable(typeof(int[]))]
[JsonSerializable(typeof(string))]
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.