From 3551cd485c8f8a897fe6a66702c18271836ed35b Mon Sep 17 00:00:00 2001
From: Jhih-Lin Jhou <103354@smsassist.com>
Date: Fri, 5 Sep 2025 16:39:06 -0500
Subject: [PATCH 1/2] add function exception log attribute
---
 .../LogAttributes/FnExceptionLogAttribute.cs  | 188 ++++++++++++++++++
 1 file changed, 188 insertions(+)
 create mode 100644 src/Infrastructure/BotSharp.Core/Infrastructures/LogAttributes/FnExceptionLogAttribute.cs
diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/LogAttributes/FnExceptionLogAttribute.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/LogAttributes/FnExceptionLogAttribute.cs
new file mode 100644
index 000000000..9217194f9
--- /dev/null
+++ b/src/Infrastructure/BotSharp.Core/Infrastructures/LogAttributes/FnExceptionLogAttribute.cs
@@ -0,0 +1,188 @@
+using BotSharp.Abstraction.Functions;
+using Microsoft.Extensions.Logging.Abstractions;
+using Rougamo;
+using Rougamo.Context;
+using Rougamo.Metadatas;
+using System.Reflection;
+
+namespace BotSharp.Core.Infrastructures.Log;
+
+/// 
+/// Shared Rougamo-based logging attribute for IFunctionCallback implementations that captures
+/// method execution details, parameters, and BotSharp-specific context.
+/// This attribute can be used across all BotSharp plugins for consistent function logging.
+/// 
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
+[Advice(Feature.OnException)]
+public class FnExceptionLogAttribute : AsyncMoAttribute
+{
+    private readonly bool _logArguments;
+
+    public FnExceptionLogAttribute(
+        bool logArguments = true)
+    {
+        _logArguments = logArguments;
+    }
+
+    public override async ValueTask OnExceptionAsync(MethodContext context)
+    {
+        var logger = GetLogger(context);
+        var functionContext = GetFunctionContext(context);
+        LogMethodError(logger, context, functionContext, context?.Exception);
+
+        await ValueTask.CompletedTask;
+    }
+
+    private ILogger? GetLogger(MethodContext context)
+    {
+        try
+        {
+            var target = context.Target;
+            var targetType = target?.GetType();
+
+            if (targetType == null) return NullLogger.Instance;
+
+            var loggerField = targetType?.GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+                .FirstOrDefault(f => f.FieldType == typeof(ILogger) ||
+                                    (f.FieldType.IsGenericType &&
+                                     f.FieldType.GetGenericTypeDefinition() == typeof(ILogger<>)));
+
+            if (loggerField != null)
+            {
+                return loggerField.GetValue(target) as ILogger;
+            }
+
+            var serviceProviderField = targetType?.GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+                .FirstOrDefault(f => f.FieldType == typeof(IServiceProvider));
+
+            if (serviceProviderField != null)
+            {
+                var serviceProvider = serviceProviderField.GetValue(target) as IServiceProvider;
+                var loggerFactory = serviceProvider?.GetService();
+                return loggerFactory?.CreateLogger(targetType) ??
+                       NullLogger.Instance;
+            }
+
+            return NullLogger.Instance;
+        }
+        catch
+        {
+            return NullLogger.Instance;
+        }
+    }
+
+    private FunctionContext GetFunctionContext(MethodContext context)
+    {
+        var functionContext = new FunctionContext();
+
+        try
+        {
+            var target = context.Target;
+            var targetType = target?.GetType();
+            functionContext.MethodFullName = $"{targetType?.Name}.{context.Method.Name}";
+
+            var serviceProviderField = targetType?.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
+                .FirstOrDefault(f => f.FieldType == typeof(IServiceProvider));
+
+            if (serviceProviderField != null)
+            {
+                var serviceProvider = serviceProviderField.GetValue(target) as IServiceProvider;
+                var stateService = serviceProvider?.GetService();
+
+                if (stateService != null)
+                {
+                    functionContext.ConversationId = stateService.GetConversationId() ?? "unknown";
+                    functionContext.Channel = stateService.GetState("channel", "unknown");
+                    functionContext.CurrentAgentId = stateService.GetState("current_agent_id", string.Empty);
+                }
+            }
+
+            if (target is IFunctionCallback callback)
+            {
+                functionContext.FunctionName = callback.Name ?? "unknown";
+            }
+
+            // Enhanced message argument processing
+            if (context.Arguments?.FirstOrDefault(arg => arg is RoleDialogModel) is RoleDialogModel messageArg)
+            {
+                functionContext.MessageId = messageArg.MessageId ?? "unknown";
+                functionContext.CurrentAgentId = messageArg.CurrentAgentId ?? functionContext.CurrentAgentId;
+                functionContext.FunctionName = messageArg.FunctionName ?? functionContext.FunctionName;
+            }
+        }
+        catch (Exception ex)
+        {
+            functionContext.ContextError = $"Context extraction failed: {ex.Message}";
+        }
+
+        return functionContext;
+    }
+
+    private void LogMethodError(ILogger? logger, MethodContext context, FunctionContext functionContext, Exception? ex)
+    {
+        if (logger == null || !logger.IsEnabled(LogLevel.Error)) return;
+
+        var argumentsSummary = string.Empty;
+        if (_logArguments && context?.Arguments?.Length > 0)
+        {
+            argumentsSummary = string.Join(", ", context.Arguments.Select((arg, i) =>
+                $"arg{i}: {GetArgumentSummary(arg)}"));
+        }
+
+        logger.LogError(ex,
+                "[FUNCTION_ERROR] {MethodName} | ConvId: {ConversationId} | Channel: {Channel} | AgentId: {AgentId} | Function: {FunctionName} | MsgId: {MessageId} | Exception: {ExceptionType} | Message: {ExceptionMessage}{ArgumentsInfo}",
+                functionContext.MethodFullName,
+                functionContext.ConversationId,
+                functionContext.Channel,
+                functionContext.CurrentAgentId,
+                functionContext.FunctionName,
+                functionContext.MessageId,
+                ex?.GetType().Name ?? "Unknown",
+                ex?.Message ?? "No message",
+                !string.IsNullOrEmpty(argumentsSummary) ? $" | Args: [{argumentsSummary}]" : "");
+    }
+
+    private string GetArgumentSummary(object arg)
+    {
+        if (arg == null) return "null";
+
+        try
+        {
+            if (arg is string str)
+            {
+                return $"\"{str}\"";
+            }
+
+            if (arg is RoleDialogModel message)
+            {
+                return $"RoleDialogModel(Role: {message.Role}, Content: {message.Content})";
+            }
+
+            if (arg.GetType().IsPrimitive || arg is decimal || arg is DateTime)
+            {
+                return arg.ToString();
+            }
+
+            var json = JsonSerializer.Serialize(arg);
+            return json;
+        }
+        catch
+        {
+            return $"{arg.GetType().Name}(...)";
+        }
+    }
+
+    /// 
+    /// Function context with additional metadata
+    /// 
+    private class FunctionContext
+    {
+        public string ConversationId { get; set; } = string.Empty;
+        public string Channel { get; set; } = string.Empty;
+        public string CurrentAgentId { get; set; } = string.Empty;
+        public string FunctionName { get; set; } = string.Empty;
+        public string MessageId { get; set; } = string.Empty;
+        public string ContextError { get; set; } = string.Empty;
+        public string MethodFullName { get; set; } = string.Empty;
+    }
+}
\ No newline at end of file
From f270f47b928032fd4504159df63cd328d262ca19 Mon Sep 17 00:00:00 2001
From: Jhih-Lin Jhou <103354@smsassist.com>
Date: Fri, 5 Sep 2025 16:45:52 -0500
Subject: [PATCH 2/2] update comment
---
 .../Infrastructures/LogAttributes/FnExceptionLogAttribute.cs   | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/Infrastructure/BotSharp.Core/Infrastructures/LogAttributes/FnExceptionLogAttribute.cs b/src/Infrastructure/BotSharp.Core/Infrastructures/LogAttributes/FnExceptionLogAttribute.cs
index 9217194f9..adf7a8fef 100644
--- a/src/Infrastructure/BotSharp.Core/Infrastructures/LogAttributes/FnExceptionLogAttribute.cs
+++ b/src/Infrastructure/BotSharp.Core/Infrastructures/LogAttributes/FnExceptionLogAttribute.cs
@@ -8,8 +8,7 @@
 namespace BotSharp.Core.Infrastructures.Log;
 
 /// 
-/// Shared Rougamo-based logging attribute for IFunctionCallback implementations that captures
-/// method execution details, parameters, and BotSharp-specific context.
+/// Use Rougamo-based logging attribute to captures method exception details, parameters, and BotSharp-specific context
 /// This attribute can be used across all BotSharp plugins for consistent function logging.
 /// 
 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]