From d7e0a53caa34b646ec2aa5118212349fc50805e3 Mon Sep 17 00:00:00 2001 From: TheeJamesLee Date: Thu, 29 Oct 2026 11:07:44 +0000 Subject: [PATCH 1/4] Adds r_Rationale varience Linked to https://github.com/netclaw-dev/netclaw/pull/2705 Signed-off-by: TheeJamesLee --- src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs b/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs index 4595fa9998..b85561651c 100644 --- a/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs +++ b/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs @@ -117,6 +117,7 @@ private static Dictionary BuildNormalizedMetaFields() map["timeout"] = "_timeout_seconds"; map["rational"] = "_rationale"; map["rationalle"] = "_rationale"; + map["r_rationale"] = "_rationale"; return map; } From 1cd1e2675e526830a73688709a34befb26281b80 Mon Sep 17 00:00:00 2001 From: James Lee Date: Fri, 30 Oct 2026 13:58:55 +0000 Subject: [PATCH 2/4] fix: Tool calling metadata field names can be mis-spelt by LLMs. Applies fuzzy matching against the ToolCallMeta.MetaFieldNames to avoid having to strongly type all differences with a suitable threshold. Tests ensure fuzzymatching still wired for known mis-types --- .../Tools/MetaFieldResolutionTests.cs | 2 + .../FuzzyMatcher.cs | 58 +++++++++++++++++++ .../ToolArgumentHelper.cs | 17 ++++-- 3 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 src/Netclaw.Tools.Abstractions/FuzzyMatcher.cs diff --git a/src/Netclaw.Actors.Tests/Tools/MetaFieldResolutionTests.cs b/src/Netclaw.Actors.Tests/Tools/MetaFieldResolutionTests.cs index cb4f8e1070..137d446c12 100644 --- a/src/Netclaw.Actors.Tests/Tools/MetaFieldResolutionTests.cs +++ b/src/Netclaw.Actors.Tests/Tools/MetaFieldResolutionTests.cs @@ -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")] diff --git a/src/Netclaw.Tools.Abstractions/FuzzyMatcher.cs b/src/Netclaw.Tools.Abstractions/FuzzyMatcher.cs new file mode 100644 index 0000000000..65500859a4 --- /dev/null +++ b/src/Netclaw.Tools.Abstractions/FuzzyMatcher.cs @@ -0,0 +1,58 @@ +using System.Linq; + +namespace Netclaw.Tools +{ + public static class FuzzyMatcher + { + public static string? FindBestMatch(string input, System.Collections.Generic.IEnumerable 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]; + } + } +} diff --git a/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs b/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs index b85561651c..91c84cd621 100644 --- a/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs +++ b/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs @@ -114,10 +114,9 @@ private static Dictionary BuildNormalizedMetaFields() foreach (var canonical in ToolCallMeta.MetaFieldNames) map[NormalizeKey(canonical)] = canonical; - map["timeout"] = "_timeout_seconds"; - map["rational"] = "_rationale"; - map["rationalle"] = "_rationale"; - map["r_rationale"] = "_rationale"; + //map["timeout"] = "_timeout_seconds"; + //map["rational"] = "_rationale"; + //map["rationalle"] = "_rationale"; return map; } @@ -133,7 +132,15 @@ private static Dictionary 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? arguments, string key) { From 2098e1b4a860bf4df553aa008d7f8d9ab72eebb7 Mon Sep 17 00:00:00 2001 From: James Lee Date: Fri, 30 Oct 2026 14:35:22 +0000 Subject: [PATCH 3/4] Hard typed variations not required and fully removed. --- src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs b/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs index 91c84cd621..f488855c12 100644 --- a/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs +++ b/src/Netclaw.Tools.Abstractions/ToolArgumentHelper.cs @@ -112,11 +112,7 @@ private static Dictionary BuildNormalizedMetaFields() { var map = new Dictionary(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; } From 1b4559eba8647dffdf0f75e6f2d3828a4fcdede5 Mon Sep 17 00:00:00 2001 From: TheeJamesLee Date: Mon, 9 Nov 2026 17:39:07 +0000 Subject: [PATCH 4/4] Adds copyright header --- src/Netclaw.Tools.Abstractions/FuzzyMatcher.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Netclaw.Tools.Abstractions/FuzzyMatcher.cs b/src/Netclaw.Tools.Abstractions/FuzzyMatcher.cs index 65500859a4..4140141521 100644 --- a/src/Netclaw.Tools.Abstractions/FuzzyMatcher.cs +++ b/src/Netclaw.Tools.Abstractions/FuzzyMatcher.cs @@ -1,3 +1,9 @@ + +// ----------------------------------------------------------------------- +// +// Copyright (C) 2026 - 2026 Petabridge, LLC +// +// ----------------------------------------------------------------------- using System.Linq; namespace Netclaw.Tools