Skip to content
2 changes: 2 additions & 0 deletions src/Netclaw.Actors.Tests/Tools/MetaFieldResolutionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ public class MetaFieldResolutionTests
[Theory]
[InlineData("_rationale", "_rationale")]
[InlineData("Rationale", "_rationale")]
[InlineData("r_rationale", "_rationale")]
[InlineData("rationale", "_rationale")]
[InlineData("rationalle", "_rationale")]
[InlineData("_timeout_seconds", "_timeout_seconds")]
[InlineData("TimeoutSeconds", "_timeout_seconds")]
[InlineData("timeout_seconds", "_timeout_seconds")]
Expand Down
64 changes: 64 additions & 0 deletions src/Netclaw.Tools.Abstractions/FuzzyMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

// -----------------------------------------------------------------------
// <copyright file="FuzzyMatcher.cs" company="Petabridge, LLC">
// Copyright (C) 2026 - 2026 Petabridge, LLC <https://petabridge.com>
// </copyright>
// -----------------------------------------------------------------------
using System.Linq;

namespace Netclaw.Tools
{
public static class FuzzyMatcher
{
public static string? FindBestMatch(string input, System.Collections.Generic.IEnumerable<string> candidates, double threshold = 0.75)
{
if (string.IsNullOrWhiteSpace(input)) return null;
var normalisedInput = Normalise(input);
if (normalisedInput.Length == 0) return null;

string? bestMatch = null;
double bestScore = 0;

foreach (var candidate in candidates)
{
var normalisedCandidate = Normalise(candidate);
if (normalisedCandidate.Length == 0) continue;
double score = Similarity(normalisedInput, normalisedCandidate);
if (normalisedCandidate.Contains(normalisedInput, StringComparison.Ordinal)) score = System.Math.Max(score, 0.85);
if (score > bestScore)
{
bestScore = score;
bestMatch = candidate;
}
}

return bestScore >= threshold ? bestMatch : null;
}

private static string Normalise(string value) =>
new string(value.Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray());

private static double Similarity(string a, string b)
{
int distance = LevenshteinDistance(a, b);
return 1.0 - (double)distance / System.Math.Max(a.Length, b.Length);
}

private static int LevenshteinDistance(string a, string b)
{
var previous = System.Linq.Enumerable.Range(0, b.Length + 1).ToArray();
var current = new int[b.Length + 1];
for (int i = 1; i <= a.Length; i++)
{
current[0] = i;
for (int j = 1; j <= b.Length; j++)
{
int cost = a[i - 1] == b[j - 1] ? 0 : 1;
current[j] = System.Math.Min(System.Math.Min(current[j - 1] + 1, previous[j] + 1), previous[j - 1] + cost);
}
(previous, current) = (current, previous);
}
return previous[b.Length];
}
}
}
16 changes: 10 additions & 6 deletions src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,7 @@ private static Dictionary<string, string> BuildNormalizedMetaFields()
{
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var canonical in ToolCallMeta.MetaFieldNames)
map[NormalizeKey(canonical)] = canonical;

map["timeout"] = "_timeout_seconds";
map["rational"] = "_rationale";
map["rationalle"] = "_rationale";
map[NormalizeKey(canonical)] = canonical;
return map;
}

Expand All @@ -132,7 +128,15 @@ private static Dictionary<string, string> BuildNormalizedMetaFields()
public static string? ResolveMetaField(string? key)
=> string.IsNullOrWhiteSpace(key)
? null
: NormalizedMetaFields.TryGetValue(NormalizeKey(key), out var canonical) ? canonical : null;
: NormalizedMetaFields.TryGetValue(NormalizeKey(key), out var canonical)
? canonical
: FuzzyMatchUnmatched(key);

private static string? FuzzyMatchUnmatched(string key)
{
const double threshold = 0.75;
return FuzzyMatcher.FindBestMatch(key, ToolCallMeta.MetaFieldNames, threshold);
}

public static string? GetString(IDictionary<string, object?>? arguments, string key)
{
Expand Down