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