< Summary

Information
Class: Intentum.AI.ONNX.OnnxIntentModel
Assembly: Intentum.AI.ONNX
File(s): /home/runner/work/Intentum/Intentum/src/Intentum.AI.ONNX/OnnxIntentModel.cs
Line coverage
92%
Covered lines: 81
Uncovered lines: 7
Coverable lines: 88
Total lines: 140
Line coverage: 92%
Branch coverage
80%
Covered branches: 40
Total branches: 50
Branch coverage: 80%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)90%2020100%
Infer(...)62.5%8895%
Dispose()100%11100%
VectorToFloats(...)50%6668.75%
LogitsToIntent(...)80%101095.45%
ConfidenceLevel(...)100%66100%

File(s)

/home/runner/work/Intentum/Intentum/src/Intentum.AI.ONNX/OnnxIntentModel.cs

#LineLine coverage
 1using Intentum.Core.Behavior;
 2using Intentum.Core.Contracts;
 3using Intentum.Core.Intents;
 4using Microsoft.ML.OnnxRuntime;
 5
 6namespace Intentum.AI.ONNX;
 7
 8/// <summary>
 9/// Local intent classifier using ONNX Runtime. Implements <see cref="IIntentModel"/> for offline or low-latency inferen
 10/// Expects model input: float tensor [1, N]; output: float tensor [1, C] (logits). Maps argmax to intent name and softm
 11/// </summary>
 12public sealed class OnnxIntentModel : IIntentModel, IDisposable
 13{
 14    private readonly InferenceSession _session;
 15    private readonly OnnxIntentModelOptions _options;
 16    private readonly string _inputName;
 17    private readonly string _outputName;
 18    private readonly int _inputSize;
 19
 20    /// <summary>
 21    /// Creates an ONNX intent model from the given options. Loads the model file and validates input/output shapes.
 22    /// </summary>
 723    public OnnxIntentModel(OnnxIntentModelOptions options)
 724    {
 725        _options = options ?? throw new ArgumentNullException(nameof(options));
 626        if (string.IsNullOrWhiteSpace(options.ModelPath) || !File.Exists(options.ModelPath))
 127            throw new ArgumentException($"Model file not found: {options.ModelPath}", nameof(options));
 528        if (options.IntentLabels is not { Count: > 0 })
 129            throw new ArgumentException("IntentLabels must be non-empty.", nameof(options));
 30
 431        _session = new InferenceSession(options.ModelPath);
 32
 433        _inputName = options.InputName ?? _session.InputNames[0];
 434        _outputName = options.OutputName ?? _session.OutputNames[0];
 35
 436        var inputMeta = _session.InputMetadata[_inputName];
 437        var outputMeta = _session.OutputMetadata[_outputName];
 38
 439        _inputSize = Convert.ToInt32(inputMeta.Dimensions[^1]);
 440        var numClasses = Convert.ToInt32(outputMeta.Dimensions[^1]);
 41
 442        if (options.IntentLabels.Count != numClasses)
 143            throw new ArgumentException(
 144                $"IntentLabels count ({options.IntentLabels.Count}) must match model output size ({numClasses}).",
 145                nameof(options));
 46
 347        if (options.FeatureDimensionNames != null && options.FeatureDimensionNames.Count != _inputSize)
 148            throw new ArgumentException(
 149                $"FeatureDimensionNames count ({options.FeatureDimensionNames.Count}) must match model input size ({_inp
 150                nameof(options));
 251    }
 52
 53    /// <inheritdoc />
 54    public Intent Infer(BehaviorSpace behaviorSpace, BehaviorVector? precomputedVector = null)
 255    {
 256        var vector = precomputedVector ?? behaviorSpace.ToVector();
 257        var inputFloats = VectorToFloats(vector);
 58
 259        using var inputOrt = OrtValue.CreateTensorValueFromMemory(inputFloats, [1L, _inputSize]);
 260        var inputs = new Dictionary<string, OrtValue> { { _inputName, inputOrt } };
 261        var outputNames = new[] { _outputName };
 62
 263        using var runOptions = new RunOptions();
 264        using var outputs = _session.Run(runOptions, inputs, outputNames);
 265        if (outputs.Count == 0)
 066            return new Intent("Unknown", [], new IntentConfidence(0, "None"), "ONNX output missing.");
 67
 268        var (intentIndex, confidence) = LogitsToIntent(outputs[0]);
 69
 270        var intentName = intentIndex >= 0 && intentIndex < _options.IntentLabels.Count
 271            ? _options.IntentLabels[intentIndex]
 272            : "Unknown";
 73
 274        return new Intent(
 275            intentName,
 276            [],
 277            new IntentConfidence(confidence, ConfidenceLevel(confidence)),
 278            $"ONNX index {intentIndex}");
 279    }
 80
 81    /// <inheritdoc />
 282    public void Dispose() => _session.Dispose();
 83
 84    private float[] VectorToFloats(BehaviorVector vector)
 285    {
 286        var dims = vector.Dimensions;
 287        var values = new float[_inputSize];
 88
 289        if (_options.FeatureDimensionNames != null)
 090        {
 091            var names = _options.FeatureDimensionNames;
 092            for (var i = 0; i < names.Count; i++)
 093                values[i] = Convert.ToSingle(dims.GetValueOrDefault(names[i], 0.0));
 094        }
 95        else
 296        {
 497            var keys = dims.Keys.OrderBy(k => k, StringComparer.Ordinal).ToList();
 1098            for (var i = 0; i < Math.Min(keys.Count, _inputSize); i++)
 399                values[i] = Convert.ToSingle(dims[keys[i]]);
 2100        }
 101
 2102        return values;
 2103    }
 104
 105    private static (int IntentIndex, double Confidence) LogitsToIntent(OrtValue outputOrt)
 3106    {
 3107        var span = outputOrt.GetTensorDataAsSpan<float>();
 3108        if (span.Length == 0)
 0109            return (-1, 0);
 110
 3111        var logits = new float[span.Length];
 3112        span.CopyTo(logits);
 113
 3114        var maxIdx = 0;
 3115        var maxLogit = logits[0];
 18116        for (var i = 1; i < logits.Length; i++)
 6117        {
 6118            if (logits[i] > maxLogit)
 1119            {
 1120                maxLogit = logits[i];
 1121                maxIdx = i;
 1122            }
 6123        }
 124
 3125        var expSum = 0.0;
 24126        for (var i = 0; i < logits.Length; i++)
 9127            expSum += Math.Exp(logits[i] - maxLogit);
 3128        var confidence = expSum > 0 ? 1.0 / expSum : 0;
 129
 3130        return (maxIdx, Math.Clamp(confidence, 0, 1));
 3131    }
 132
 133    private static string ConfidenceLevel(double score)
 10134    {
 12135        if (score >= 0.8) return "High";
 11136        if (score >= 0.5) return "Medium";
 8137        if (score >= 0.2) return "Low";
 2138        return "None";
 10139    }
 140}