| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using System.Diagnostics; |
| | | 3 | | |
| | | 4 | | using Microsoft.Agents.AI; |
| | | 5 | | |
| | | 6 | | using NexusLabs.Needlr.AgentFramework.Diagnostics; |
| | | 7 | | using NexusLabs.Needlr.AgentFramework.Progress; |
| | | 8 | | |
| | | 9 | | namespace NexusLabs.Needlr.AgentFramework.Workflows.Diagnostics; |
| | | 10 | | |
| | | 11 | | /// <summary> |
| | | 12 | | /// Innermost middleware layer: wraps each tool/function invocation to capture per-call |
| | | 13 | | /// timing and custom metrics. Emits <see cref="ToolCallStartedEvent"/> and |
| | | 14 | | /// <see cref="ToolCallCompletedEvent"/> to the progress reporter in real-time. |
| | | 15 | | /// </summary> |
| | | 16 | | internal static class DiagnosticsFunctionCallingMiddleware |
| | | 17 | | { |
| | | 18 | | internal static void Wire( |
| | | 19 | | AIAgentBuilder builder, |
| | | 20 | | IAgentMetrics metrics, |
| | | 21 | | IProgressReporterAccessor progressAccessor, |
| | | 22 | | IToolCallCollector? toolCallCollector = null) |
| | | 23 | | { |
| | 39 | 24 | | FunctionInvocationDelegatingAgentBuilderExtensions.Use( |
| | 39 | 25 | | builder, |
| | 39 | 26 | | async (agent, context, next, cancellationToken) => |
| | 39 | 27 | | { |
| | 0 | 28 | | var diagnosticsBuilder = AgentRunDiagnosticsBuilder.GetCurrent(); |
| | 0 | 29 | | var sequence = diagnosticsBuilder?.NextToolCallSequence() ?? -1; |
| | 0 | 30 | | var startedAt = DateTimeOffset.UtcNow; |
| | 0 | 31 | | var stopwatch = Stopwatch.StartNew(); |
| | 39 | 32 | | |
| | 0 | 33 | | var toolName = context.Function?.Name ?? "unknown"; |
| | 39 | 34 | | |
| | 0 | 35 | | using var activity = metrics.ActivitySource.StartActivity($"agent.tool {toolName}", ActivityKind.Interna |
| | 0 | 36 | | activity?.SetTag("agent.tool.name", toolName); |
| | 0 | 37 | | activity?.SetTag("agent.tool.sequence", sequence); |
| | 0 | 38 | | activity?.SetTag("gen_ai.agent.name", diagnosticsBuilder?.AgentName); |
| | 39 | 39 | | |
| | 0 | 40 | | progressAccessor.Current.Report(new ToolCallStartedEvent( |
| | 0 | 41 | | Timestamp: startedAt, |
| | 0 | 42 | | WorkflowId: progressAccessor.Current.WorkflowId, |
| | 0 | 43 | | AgentId: progressAccessor.Current.AgentId, |
| | 0 | 44 | | ParentAgentId: diagnosticsBuilder?.ParentAgentName, |
| | 0 | 45 | | Depth: progressAccessor.Current.Depth, |
| | 0 | 46 | | SequenceNumber: progressAccessor.Current.NextSequence(), |
| | 0 | 47 | | ToolName: toolName)); |
| | 39 | 48 | | |
| | 0 | 49 | | var customMetrics = new ConcurrentDictionary<string, object?>(StringComparer.OrdinalIgnoreCase); |
| | 0 | 50 | | ToolMetricsAccessor.CurrentToolMetrics.Value = customMetrics; |
| | 39 | 51 | | |
| | 39 | 52 | | try |
| | 39 | 53 | | { |
| | 0 | 54 | | var result = await next(context, cancellationToken).ConfigureAwait(false); |
| | 0 | 55 | | stopwatch.Stop(); |
| | 39 | 56 | | |
| | 0 | 57 | | activity?.SetTag("status", "success"); |
| | 39 | 58 | | |
| | 0 | 59 | | if (activity is not null && customMetrics.Count > 0) |
| | 39 | 60 | | { |
| | 0 | 61 | | foreach (var (key, value) in customMetrics) |
| | 0 | 62 | | activity.SetTag($"tool.custom.{key}", value); |
| | 39 | 63 | | } |
| | 39 | 64 | | |
| | 0 | 65 | | metrics.RecordToolCall(toolName, stopwatch.Elapsed, succeeded: true, agentName: diagnosticsBuilder?. |
| | 39 | 66 | | |
| | 0 | 67 | | var toolDiag = new ToolCallDiagnostics( |
| | 0 | 68 | | Sequence: sequence, |
| | 0 | 69 | | ToolName: toolName, |
| | 0 | 70 | | Duration: stopwatch.Elapsed, |
| | 0 | 71 | | Succeeded: true, |
| | 0 | 72 | | ErrorMessage: null, |
| | 0 | 73 | | StartedAt: startedAt, |
| | 0 | 74 | | CompletedAt: DateTimeOffset.UtcNow, |
| | 0 | 75 | | CustomMetrics: customMetrics.Count > 0 ? customMetrics : null) |
| | 0 | 76 | | { |
| | 0 | 77 | | AgentName = diagnosticsBuilder?.AgentName, |
| | 0 | 78 | | Arguments = SnapshotArguments(context.Arguments), |
| | 0 | 79 | | Result = result, |
| | 0 | 80 | | ArgumentsCharCount = DiagnosticsCharCounter.JsonLength(SnapshotArguments(context.Arguments)), |
| | 0 | 81 | | ResultCharCount = DiagnosticsCharCounter.JsonLength(result), |
| | 0 | 82 | | }; |
| | 0 | 83 | | diagnosticsBuilder?.AddToolCall(toolDiag); |
| | 0 | 84 | | (toolCallCollector as ToolCallCollector)?.Add(toolDiag); |
| | 39 | 85 | | |
| | 0 | 86 | | progressAccessor.Current.Report(new ToolCallCompletedEvent( |
| | 0 | 87 | | Timestamp: DateTimeOffset.UtcNow, |
| | 0 | 88 | | WorkflowId: progressAccessor.Current.WorkflowId, |
| | 0 | 89 | | AgentId: progressAccessor.Current.AgentId, |
| | 0 | 90 | | ParentAgentId: diagnosticsBuilder?.ParentAgentName, |
| | 0 | 91 | | Depth: progressAccessor.Current.Depth, |
| | 0 | 92 | | SequenceNumber: progressAccessor.Current.NextSequence(), |
| | 0 | 93 | | ToolName: toolName, |
| | 0 | 94 | | Duration: stopwatch.Elapsed, |
| | 0 | 95 | | CustomMetrics: customMetrics.Count > 0 ? customMetrics : null)); |
| | 39 | 96 | | |
| | 0 | 97 | | return result; |
| | 39 | 98 | | } |
| | 0 | 99 | | catch (Exception ex) |
| | 39 | 100 | | { |
| | 0 | 101 | | stopwatch.Stop(); |
| | 39 | 102 | | |
| | 0 | 103 | | activity?.SetStatus(ActivityStatusCode.Error, ex.Message); |
| | 0 | 104 | | activity?.SetTag("status", "failed"); |
| | 39 | 105 | | |
| | 0 | 106 | | metrics.RecordToolCall(toolName, stopwatch.Elapsed, succeeded: false, agentName: diagnosticsBuilder? |
| | 39 | 107 | | |
| | 0 | 108 | | var failedToolDiag = new ToolCallDiagnostics( |
| | 0 | 109 | | Sequence: sequence, |
| | 0 | 110 | | ToolName: toolName, |
| | 0 | 111 | | Duration: stopwatch.Elapsed, |
| | 0 | 112 | | Succeeded: false, |
| | 0 | 113 | | ErrorMessage: ex.Message, |
| | 0 | 114 | | StartedAt: startedAt, |
| | 0 | 115 | | CompletedAt: DateTimeOffset.UtcNow, |
| | 0 | 116 | | CustomMetrics: customMetrics.Count > 0 ? customMetrics : null) |
| | 0 | 117 | | { |
| | 0 | 118 | | AgentName = diagnosticsBuilder?.AgentName, |
| | 0 | 119 | | Arguments = SnapshotArguments(context.Arguments), |
| | 0 | 120 | | ArgumentsCharCount = DiagnosticsCharCounter.JsonLength(SnapshotArguments(context.Arguments)), |
| | 0 | 121 | | }; |
| | 0 | 122 | | diagnosticsBuilder?.AddToolCall(failedToolDiag); |
| | 0 | 123 | | (toolCallCollector as ToolCallCollector)?.Add(failedToolDiag); |
| | 39 | 124 | | |
| | 0 | 125 | | progressAccessor.Current.Report(new ToolCallFailedEvent( |
| | 0 | 126 | | Timestamp: DateTimeOffset.UtcNow, |
| | 0 | 127 | | WorkflowId: progressAccessor.Current.WorkflowId, |
| | 0 | 128 | | AgentId: progressAccessor.Current.AgentId, |
| | 0 | 129 | | ParentAgentId: diagnosticsBuilder?.ParentAgentName, |
| | 0 | 130 | | Depth: progressAccessor.Current.Depth, |
| | 0 | 131 | | SequenceNumber: progressAccessor.Current.NextSequence(), |
| | 0 | 132 | | ToolName: toolName, |
| | 0 | 133 | | ErrorMessage: ex.Message, |
| | 0 | 134 | | Duration: stopwatch.Elapsed)); |
| | 39 | 135 | | |
| | 0 | 136 | | throw; |
| | 39 | 137 | | } |
| | 39 | 138 | | finally |
| | 39 | 139 | | { |
| | 0 | 140 | | ToolMetricsAccessor.CurrentToolMetrics.Value = null; |
| | 39 | 141 | | } |
| | 39 | 142 | | }); |
| | 39 | 143 | | } |
| | | 144 | | |
| | | 145 | | private static IReadOnlyDictionary<string, object?>? SnapshotArguments( |
| | | 146 | | IDictionary<string, object?>? arguments) |
| | | 147 | | { |
| | 0 | 148 | | if (arguments is null || arguments.Count == 0) |
| | | 149 | | { |
| | 0 | 150 | | return null; |
| | | 151 | | } |
| | | 152 | | |
| | 0 | 153 | | return new Dictionary<string, object?>(arguments); |
| | | 154 | | } |
| | | 155 | | } |