diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/DiagnosticHelper.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/DiagnosticHelper.cs new file mode 100644 index 000000000..d087cb2c1 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/DiagnosticHelper.cs @@ -0,0 +1,16 @@ +using System.Diagnostics; + +namespace BotSharp.Abstraction.Utilities; + +public static class DiagnosticHelper +{ + /// + /// Gets the current stack trace for debugging purposes. + /// + /// Number of frames to skip (default 1 to skip this method itself) + /// Stack trace string + public static string GetCurrentStackTrace(int skipFrames = 1) + { + return new StackTrace(skipFrames, true).ToString(); + } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs index 20ff3dc51..ce4e010bf 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Utilities/StringExtensions.cs @@ -64,6 +64,44 @@ public static string CleanStr(this string? str) return str.Replace(" ", "").Replace("\t", "").Replace("\n", "").Replace("\r", ""); } + /// + /// Normalizes function name by removing namespace/agent prefixes. + /// LLM sometimes returns function names like "AgentName.FunctionName" or "Namespace.FunctionName". + /// This method extracts the actual function name. Returns original input if normalization would + /// yield empty/whitespace (e.g. "Agent." or "Agent/ ") to avoid downstream invalid function lookup. + /// + /// The raw function name from LLM response + /// The normalized function name, or original if normalized would be empty/whitespace + public static string? NormalizeFunctionName(this string? functionName) + { + functionName = functionName?.Trim(); + if (string.IsNullOrEmpty(functionName)) + { + return functionName; + } + + foreach (var separator in new[] { '.', '/' }) + { + if (!functionName.Contains(separator)) + { + continue; + } + + var segments = functionName.Split(separator); + var normalized = segments.LastOrDefault()?.Trim(); + + if (string.IsNullOrWhiteSpace(normalized)) + { + return functionName; + } + + Console.WriteLine($"NormalizeFunctionName: {functionName} -> {normalized}"); + return normalized; + } + + return functionName; + } + public static string CleanJsonStr(this string? str) { if (string.IsNullOrWhiteSpace(str)) diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs index 9dd19c09b..c71d0f07e 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/InstructExecutor.cs @@ -31,7 +31,7 @@ await HookEmitter.Emit(_services, async hook => await hook.OnRouti } message.FunctionArgs = JsonSerializer.Serialize(inst); - if (message.FunctionName != null) + if (!string.IsNullOrEmpty(message.FunctionName)) { var msg = RoleDialogModel.From(message, role: AgentRole.Function); await routing.InvokeFunction(message.FunctionName, msg, options: new() { From = InvokeSource.Routing }); diff --git a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs index 4c138268b..7df56f7f5 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Reasoning/ReasonerHelper.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Utilities; + namespace BotSharp.Core.Routing.Reasoning; public static class ReasonerHelper @@ -43,10 +45,10 @@ public static async Task FixMalformedResponse(IServiceProvider services, Functio } // Function name shouldn't contain dot symbol - if (!string.IsNullOrEmpty(args.Function) && - args.Function.Contains('.')) + var normalizedFunction = args.Function.NormalizeFunctionName(); + if (normalizedFunction != args.Function) { - args.Function = args.Function.Split('.').Last(); + args.Function = normalizedFunction; malformed = true; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs index fdaddc85e..b2f246fb0 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeAgent.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Routing.Models; using BotSharp.Abstraction.Templating; +using BotSharp.Abstraction.Utilities; namespace BotSharp.Core.Routing; @@ -46,13 +47,10 @@ public async Task InvokeAgent( response = await chatCompletion.GetChatCompletions(agent, dialogs); } - if (response.Role == AgentRole.Function) + if (response.Role == AgentRole.Function && !string.IsNullOrEmpty(response.FunctionName)) { message = RoleDialogModel.From(message, role: AgentRole.Function); - if (response.FunctionName != null && response.FunctionName.Contains("/")) - { - response.FunctionName = response.FunctionName.Split("/").Last(); - } + response.FunctionName = response.FunctionName.NormalizeFunctionName(); message.ToolCallId = response.ToolCallId; message.FunctionName = response.FunctionName; message.FunctionArgs = response.FunctionArgs; diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index 3850dcc13..dc6aeacb4 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -18,7 +18,7 @@ public async Task InvokeFunction(string name, RoleDialogModel message, Inv { message.StopCompletion = true; message.Content = $"Can't find function implementation of {name}."; - _logger.LogError(message.Content); + _logger.LogError($"{message.Content}, stackInfo:{DiagnosticHelper.GetCurrentStackTrace()}"); return false; } diff --git a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs index f10748c7a..53daa54d8 100644 --- a/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.AzureOpenAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,5 +1,6 @@ #pragma warning disable OPENAI001 using BotSharp.Abstraction.Conversations.Enums; +using BotSharp.Abstraction.Utilities; using BotSharp.Abstraction.Files.Utilities; using BotSharp.Abstraction.Hooks; using BotSharp.Abstraction.MessageHub.Models; @@ -75,10 +76,18 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, }; // Somethings LLM will generate a function name with agent name. - if (!string.IsNullOrEmpty(funcContextIn.FunctionName)) - { - funcContextIn.FunctionName = funcContextIn.FunctionName.Split('.').Last(); - } + funcContextIn.FunctionName = funcContextIn.FunctionName.NormalizeFunctionName(); // Execute functions await onFunctionExecuting(funcContextIn); diff --git a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs index 6349b1ed0..c35d9ed84 100644 --- a/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.DeepSeekAI/Providers/Chat/ChatCompletionProvider.cs @@ -1,5 +1,6 @@ #pragma warning disable OPENAI001 using BotSharp.Abstraction.Conversations.Enums; +using BotSharp.Abstraction.Utilities; using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Files.Models; using BotSharp.Abstraction.Files.Utilities; @@ -66,10 +67,18 @@ public async Task GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, List GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, List GetChatCompletions(Agent agent, List GetChatCompletionsAsync(Agent agent, }; // Somethings LLM will generate a function name with agent name. - if (!string.IsNullOrEmpty(funcContextIn.FunctionName)) - { - funcContextIn.FunctionName = funcContextIn.FunctionName.Split('.').Last(); - } + funcContextIn.FunctionName = funcContextIn.FunctionName.NormalizeFunctionName(); // Execute functions await onFunctionExecuting(funcContextIn); diff --git a/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs b/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs index 5f0405c17..5b5c03bda 100644 --- a/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs +++ b/src/Plugins/BotSharp.Plugin.SparkDesk/Providers/ChatCompletionProvider.cs @@ -1,5 +1,6 @@ using BotSharp.Abstraction.Agents; using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Utilities; using BotSharp.Abstraction.Conversations; using BotSharp.Abstraction.Conversations.Enums; using BotSharp.Abstraction.Loggers; @@ -131,10 +132,7 @@ public async Task GetChatCompletionsAsync(Agent agent, List