< Summary

Information
Class: Intentum.AI.Models.CatalogInferenceStep
Assembly: Intentum.AI
File(s): /home/runner/work/Intentum/Intentum/src/Intentum.AI/Models/CatalogInferenceStep.cs
Line coverage
90%
Covered lines: 56
Uncovered lines: 6
Coverable lines: 62
Total lines: 108
Line coverage: 90.3%
Branch coverage
85%
Covered branches: 17
Total branches: 20
Branch coverage: 85%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)50%44100%
Infer(...)100%11100%
InferAsync()100%110%
Classify(...)90%1010100%
AverageVectors(...)100%66100%

File(s)

/home/runner/work/Intentum/Intentum/src/Intentum.AI/Models/CatalogInferenceStep.cs

#LineLine coverage
 1using Intentum.AI.Catalog;
 2using Intentum.AI.Embeddings;
 3using Intentum.Core.Behavior;
 4using Intentum.Core.Intents;
 5using Intentum.Core.Pipeline;
 6
 7namespace Intentum.AI.Models;
 8
 9/// <summary>
 10/// AI inference step that classifies intent by comparing behavior embeddings against
 11/// a catalog of known intent definitions using cosine similarity (nearest neighbor).
 12/// Unlike LlmInferenceStep which always returns "AI-Inferred-Intent", this step
 13/// returns the actual matched intent name from the catalog.
 14/// </summary>
 15public sealed class CatalogInferenceStep : IIntentInferenceStep
 16{
 17    private readonly IIntentEmbeddingProvider _embeddingProvider;
 18    private readonly IntentCatalog _catalog;
 19
 220    public CatalogInferenceStep(
 221        IIntentEmbeddingProvider embeddingProvider,
 222        IntentCatalog catalog)
 223    {
 224        _embeddingProvider = embeddingProvider ?? throw new ArgumentNullException(nameof(embeddingProvider));
 225        _catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
 226    }
 27
 28    /// <inheritdoc />
 29    public IntentInferenceResult Infer(BehaviorSpace behaviorSpace, BehaviorVector vector)
 230    {
 231        var embeddings = vector.Dimensions.Keys
 232            .Select(_embeddingProvider.Embed)
 233            .ToList();
 34
 235        return Classify(embeddings);
 236    }
 37
 38    /// <summary>
 39    /// Async version that properly awaits embedding calls.
 40    /// </summary>
 41    public async Task<IntentInferenceResult> InferAsync(
 42        BehaviorSpace behaviorSpace,
 43        BehaviorVector vector,
 44        CancellationToken cancellationToken = default)
 045    {
 046        var tasks = vector.Dimensions.Keys
 047            .Select(k => _embeddingProvider.EmbedAsync(k, cancellationToken));
 048        var embeddings = (await Task.WhenAll(tasks)).ToList();
 49
 050        return Classify(embeddings);
 051    }
 52
 53    private IntentInferenceResult Classify(List<IntentEmbedding> embeddings)
 254    {
 555        var vectorsWithData = embeddings.Where(e => e.Vector is { Count: > 0 }).ToList();
 56
 257        string intentName = "Unknown";
 258        double bestScore = 0;
 259        string? reasoning = null;
 60
 261        if (vectorsWithData.Count > 0)
 262        {
 563            var avgVector = AverageVectors(vectorsWithData.Select(e => e.Vector!).ToList());
 264            var match = _catalog.FindBestMatch(avgVector);
 65
 266            if (match.HasValue)
 267            {
 268                intentName = match.Value.Name;
 269                bestScore = match.Value.Score;
 270                reasoning = $"Nearest catalog match: {intentName} (similarity: {bestScore:F3})";
 271            }
 272        }
 73
 274        if (Math.Abs(bestScore) < 1e-9 && embeddings.Count > 0)
 175        {
 276            bestScore = embeddings.Average(e => e.Score);
 177            reasoning = "No catalog match found; using average embedding score";
 178        }
 79
 280        var signals = embeddings.Select(e =>
 381            new IntentSignal(Source: "catalog", Description: e.Source, Weight: e.Score))
 282            .ToList();
 83
 284        return new IntentInferenceResult(
 285            Name: intentName,
 286            Score: bestScore,
 287            Signals: signals,
 288            Reasoning: reasoning
 289        );
 290    }
 91
 92    private static double[] AverageVectors(List<IReadOnlyList<double>> vectors)
 293    {
 294        var dim = vectors[0].Count;
 295        var avg = new double[dim];
 96
 1297        foreach (var vec in vectors)
 398        {
 5499            for (var i = 0; i < Math.Min(dim, vec.Count); i++)
 24100                avg[i] += vec[i];
 3101        }
 102
 36103        for (var i = 0; i < dim; i++)
 16104            avg[i] /= vectors.Count;
 105
 2106        return avg;
 2107    }
 108}