< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Evaluation.EfficiencyEvaluator
Assembly: NexusLabs.Needlr.AgentFramework.Evaluation
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Evaluation/EfficiencyEvaluator.cs
Line coverage
100%
Covered lines: 73
Uncovered lines: 0
Coverable lines: 73
Total lines: 173
Line coverage: 100%
Branch coverage
96%
Covered branches: 25
Total branches: 26
Branch coverage: 96.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
get_EvaluationMetricNames()100%11100%
EvaluateAsync(...)95.83%2424100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Evaluation/EfficiencyEvaluator.cs

#LineLine coverage
 1using Microsoft.Extensions.AI;
 2using Microsoft.Extensions.AI.Evaluation;
 3
 4using NexusLabs.Needlr.AgentFramework.Diagnostics;
 5
 6namespace NexusLabs.Needlr.AgentFramework.Evaluation;
 7
 8/// <summary>
 9/// Deterministic evaluator that scores the token efficiency and cost profile of an
 10/// agent run from the captured <see cref="IAgentRunDiagnostics"/> snapshot carried in
 11/// an <see cref="AgentRunDiagnosticsContext"/>.
 12/// </summary>
 13/// <remarks>
 14/// <para>
 15/// This evaluator never contacts a language model. It reads
 16/// <see cref="IAgentRunDiagnostics.AggregateTokenUsage"/> and
 17/// <see cref="IAgentRunDiagnostics.ToolCalls"/> to produce:
 18/// </para>
 19/// <list type="bullet">
 20///   <item><description><c>Total Tokens</c> — aggregate token count across all LLM calls.</description></item>
 21///   <item><description><c>Input Token Ratio</c> — input tokens / total tokens. High values suggest verbose prompts; lo
 22///   <item><description><c>Tokens Per Tool Call</c> — total tokens / tool call count. Measures the token cost of each t
 23///   <item><description><c>Cache Hit Ratio</c> — cached input tokens / input tokens. Higher values mean more prompt-cac
 24///   <item><description><c>Under Budget</c> — boolean. <see langword="true"/> when total tokens is strictly below the c
 25/// </list>
 26/// <para>
 27/// When no <see cref="AgentRunDiagnosticsContext"/> is present in the
 28/// <c>additionalContext</c> collection, the evaluator returns an empty
 29/// <see cref="EvaluationResult"/> — callers should treat that as "not applicable".
 30/// </para>
 31/// </remarks>
 32/// <example>
 33/// <code>
 34/// // Score efficiency with a 10,000-token budget
 35/// var evaluator = new EfficiencyEvaluator(tokenBudget: 10_000);
 36/// var result = await evaluator.EvaluateAsync(
 37///     messages: Array.Empty&lt;ChatMessage&gt;(),
 38///     modelResponse: new ChatResponse(),
 39///     additionalContext: [new AgentRunDiagnosticsContext(diagnostics)]);
 40///
 41/// var underBudget = ((BooleanMetric)result.Metrics["Under Budget"]).Value;
 42/// var tokensPerTool = ((NumericMetric)result.Metrics["Tokens Per Tool Call"]).Value;
 43/// </code>
 44/// </example>
 45public sealed class EfficiencyEvaluator : IEvaluator
 46{
 47    /// <summary>Metric name for the aggregate token count.</summary>
 48    public const string TotalTokensMetricName = "Total Tokens";
 49
 50    /// <summary>Metric name for the input-to-total token ratio.</summary>
 51    public const string InputTokenRatioMetricName = "Input Token Ratio";
 52
 53    /// <summary>Metric name for tokens consumed per tool call.</summary>
 54    public const string TokensPerToolCallMetricName = "Tokens Per Tool Call";
 55
 56    /// <summary>Metric name for the prompt-cache hit ratio.</summary>
 57    public const string CacheHitRatioMetricName = "Cache Hit Ratio";
 58
 59    /// <summary>Metric name for the boolean budget check.</summary>
 60    public const string UnderBudgetMetricName = "Under Budget";
 61
 62    private readonly long? _tokenBudget;
 63
 64    /// <summary>
 65    /// Creates a new <see cref="EfficiencyEvaluator"/>.
 66    /// </summary>
 67    /// <param name="tokenBudget">
 68    /// Optional token budget. When provided, the evaluator emits the
 69    /// <see cref="UnderBudgetMetricName"/> metric. When <see langword="null"/>,
 70    /// the metric is omitted.
 71    /// </param>
 1272    public EfficiencyEvaluator(long? tokenBudget = null)
 73    {
 1274        _tokenBudget = tokenBudget;
 75
 1276        var names = new List<string>
 1277        {
 1278            TotalTokensMetricName,
 1279            InputTokenRatioMetricName,
 1280            TokensPerToolCallMetricName,
 1281            CacheHitRatioMetricName,
 1282        };
 1283        if (tokenBudget.HasValue)
 84        {
 485            names.Add(UnderBudgetMetricName);
 86        }
 1287        EvaluationMetricNames = names;
 1288    }
 89
 90    /// <inheritdoc />
 491    public IReadOnlyCollection<string> EvaluationMetricNames { get; }
 92
 93    /// <inheritdoc />
 94    public ValueTask<EvaluationResult> EvaluateAsync(
 95        IEnumerable<ChatMessage> messages,
 96        ChatResponse modelResponse,
 97        ChatConfiguration? chatConfiguration = null,
 98        IEnumerable<EvaluationContext>? additionalContext = null,
 99        CancellationToken cancellationToken = default)
 100    {
 10101        var diagnostics = additionalContext?
 10102            .OfType<AgentRunDiagnosticsContext>()
 10103            .FirstOrDefault()?
 10104            .Diagnostics;
 105
 10106        if (diagnostics is null)
 107        {
 1108            return new ValueTask<EvaluationResult>(new EvaluationResult());
 109        }
 110
 9111        var usage = diagnostics.AggregateTokenUsage;
 9112        var totalTokens = usage.TotalTokens;
 9113        var inputTokens = usage.InputTokens;
 9114        var cachedInputTokens = usage.CachedInputTokens;
 9115        var toolCallCount = diagnostics.ToolCalls.Count;
 116
 9117        var inputTokenRatio = totalTokens > 0
 9118            ? (double)inputTokens / totalTokens
 9119            : 0;
 120
 9121        var tokensPerToolCall = toolCallCount > 0
 9122            ? (double)totalTokens / toolCallCount
 9123            : 0;
 124
 9125        var cacheHitRatio = inputTokens > 0
 9126            ? (double)cachedInputTokens / inputTokens
 9127            : 0;
 128
 9129        var metrics = new List<EvaluationMetric>
 9130        {
 9131            new NumericMetric(
 9132                TotalTokensMetricName,
 9133                value: totalTokens,
 9134                reason: totalTokens == 0
 9135                    ? "No token usage was recorded."
 9136                    : $"{totalTokens:N0} total tokens consumed ({inputTokens:N0} input, {usage.OutputTokens:N0} output).
 9137
 9138            new NumericMetric(
 9139                InputTokenRatioMetricName,
 9140                value: inputTokenRatio,
 9141                reason: totalTokens == 0
 9142                    ? "No tokens to compute ratio."
 9143                    : $"{inputTokenRatio:P1} of tokens were input ({inputTokens:N0} of {totalTokens:N0})."),
 9144
 9145            new NumericMetric(
 9146                TokensPerToolCallMetricName,
 9147                value: tokensPerToolCall,
 9148                reason: toolCallCount == 0
 9149                    ? "No tool calls to compute per-call cost."
 9150                    : $"{tokensPerToolCall:N0} tokens per tool call ({totalTokens:N0} tokens / {toolCallCount} calls).")
 9151
 9152            new NumericMetric(
 9153                CacheHitRatioMetricName,
 9154                value: cacheHitRatio,
 9155                reason: inputTokens == 0
 9156                    ? "No input tokens to compute cache ratio."
 9157                    : $"{cacheHitRatio:P1} of input tokens were cache hits ({cachedInputTokens:N0} of {inputTokens:N0}).
 9158        };
 159
 9160        if (_tokenBudget.HasValue)
 161        {
 3162            var underBudget = totalTokens < _tokenBudget.Value;
 3163            metrics.Add(new BooleanMetric(
 3164                UnderBudgetMetricName,
 3165                value: underBudget,
 3166                reason: underBudget
 3167                    ? $"Token usage ({totalTokens:N0}) is under the budget of {_tokenBudget.Value:N0}."
 3168                    : $"Token usage ({totalTokens:N0}) reached or exceeded the budget of {_tokenBudget.Value:N0}."));
 169        }
 170
 9171        return new ValueTask<EvaluationResult>(new EvaluationResult(metrics.ToArray()));
 172    }
 173}