Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Diagnostics;

namespace BotSharp.Abstraction.Utilities;

public static class DiagnosticHelper
{
/// <summary>
/// Gets the current stack trace for debugging purposes.
/// </summary>
/// <param name="skipFrames">Number of frames to skip (default 1 to skip this method itself)</param>
/// <returns>Stack trace string</returns>
public static string GetCurrentStackTrace(int skipFrames = 1)
{
return new StackTrace(skipFrames, true).ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,44 @@ public static string CleanStr(this string? str)
return str.Replace(" ", "").Replace("\t", "").Replace("\n", "").Replace("\r", "");
}

/// <summary>
/// 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.
/// </summary>
/// <param name="functionName">The raw function name from LLM response</param>
/// <returns>The normalized function name, or original if normalized would be empty/whitespace</returns>
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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ await HookEmitter.Emit<IRoutingHook>(_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 });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using BotSharp.Abstraction.Utilities;

namespace BotSharp.Core.Routing.Reasoning;

public static class ReasonerHelper
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Templating;
using BotSharp.Abstraction.Utilities;

namespace BotSharp.Core.Routing;

Expand Down Expand Up @@ -46,13 +47,10 @@ public async Task<bool> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public async Task<bool> 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;
Comment on lines 19 to 22

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Stack trace logged in error 📘 Rule violation ⛨ Security

• The new error log appends a full stack trace via DiagnosticHelper.GetCurrentStackTrace() when a
  function implementation is missing.
• Stack traces can contain sensitive internal details (method names, paths, code locations) and the
  log line is also unstructured, making it harder to audit safely.
• This increases the risk of sensitive information exposure through logs and reduces log usability
  for monitoring/auditing.
Agent Prompt
## Issue description
The code logs a full stack trace as part of an interpolated error message. This can leak internal details into logs and is not structured logging.

## Issue Context
Secure logging requires logs to be structured and to avoid sensitive/internal details in log output.

## Fix Focus Areas
- src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs[19-22]
- src/Infrastructure/BotSharp.Abstraction/Utilities/DiagnosticHelper.cs[12-15]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -75,10 +76,18 @@ public async Task<RoleDialogModel> GetChatCompletions(Agent agent, List<RoleDial
};

// Somethings LLM will generate a function name with agent name.
if (!string.IsNullOrEmpty(responseMessage.FunctionName))
responseMessage.FunctionName = responseMessage.FunctionName.NormalizeFunctionName();
}
else if (reason == ChatFinishReason.Length)
{
_logger.LogWarning($"Action: {nameof(GetChatCompletions)}, Reason: {reason}, Agent: {agent.Name}, MaxOutputTokens: {options.MaxOutputTokenCount}");

responseMessage = new RoleDialogModel(AgentRole.Assistant, $"AI response exceeded max output length {options.MaxOutputTokenCount}")
{
responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last();
}
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
StopCompletion = true
};
}
else
{
Expand Down Expand Up @@ -200,10 +209,7 @@ public async Task<bool> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -66,10 +67,18 @@ public async Task<RoleDialogModel> GetChatCompletions(Agent agent, List<RoleDial
};

// Somethings LLM will generate a function name with agent name.
if (!string.IsNullOrEmpty(responseMessage.FunctionName))
responseMessage.FunctionName = responseMessage.FunctionName.NormalizeFunctionName();
}
else if (reason == ChatFinishReason.Length)
{
_logger.LogWarning($"Action: {nameof(GetChatCompletions)}, Reason: {reason}, Agent: {agent.Name}, MaxOutputTokens: {options.MaxOutputTokenCount}");

responseMessage = new RoleDialogModel(AgentRole.Assistant, $"AI response exceeded max output length {options.MaxOutputTokenCount}")
{
responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last();
}
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
StopCompletion = true
};
}
else
{
Expand Down Expand Up @@ -167,10 +176,7 @@ public async Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogMode
};

// 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Files.Models;
using BotSharp.Abstraction.Utilities;
using BotSharp.Abstraction.Files.Utilities;
using BotSharp.Abstraction.Hooks;
using BotSharp.Abstraction.MessageHub.Models;
Expand Down Expand Up @@ -72,6 +73,17 @@ public async Task<RoleDialogModel> GetChatCompletions(Agent agent, List<RoleDial
RenderedInstruction = string.Join("\r\n", renderedInstructions)
};
}
else if (candidate?.FinishReason == FinishReason.MAX_TOKENS)
{
_logger.LogWarning($"Action: {nameof(GetChatCompletions)}, Reason: {candidate.FinishReason}, Agent: {agent.Name}");

responseMessage = new RoleDialogModel(AgentRole.Assistant, $"AI response exceeded max output length")
{
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
StopCompletion = true
};
}
else
{
responseMessage = new RoleDialogModel(AgentRole.Assistant, text)
Expand Down Expand Up @@ -165,10 +177,7 @@ public async Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogMode
};

// 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma warning disable OPENAI001
using BotSharp.Abstraction.MessageHub.Models;
using BotSharp.Abstraction.Utilities;
using BotSharp.Core.Infrastructures.Streams;
using BotSharp.Core.MessageHub;
using OpenAI.Chat;
Expand Down Expand Up @@ -70,17 +71,17 @@ public async Task<RoleDialogModel> GetChatCompletions(Agent agent, List<RoleDial
};

// Somethings LLM will generate a function name with agent name.
if (!string.IsNullOrEmpty(responseMessage.FunctionName))
{
responseMessage.FunctionName = responseMessage.FunctionName.Split('.').Last();
}
responseMessage.FunctionName = responseMessage.FunctionName.NormalizeFunctionName();
}
else if (reason == ChatFinishReason.Length)
{
responseMessage = new RoleDialogModel(AgentRole.Function, $"AI response execeed max output length {options.MaxOutputTokenCount}")
_logger.LogWarning($"Action: {nameof(GetChatCompletions)}, Reason: {reason}, Agent: {agent.Name}, MaxOutputTokens: {options.MaxOutputTokenCount}");

responseMessage = new RoleDialogModel(AgentRole.Assistant, $"AI response exceeded max output length {options.MaxOutputTokenCount}")
{
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
StopCompletion = true
};
}
else
Expand Down Expand Up @@ -182,10 +183,7 @@ public async Task<bool> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -131,10 +132,7 @@ public async Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogMode
};

// 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);
Expand Down
Loading