| | | 1 | | using Intentum.Persistence.Repositories; |
| | | 2 | | |
| | | 3 | | namespace Intentum.Clustering; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Default implementation of intent clustering using pattern-based and score-based grouping. |
| | | 7 | | /// </summary> |
| | | 8 | | public sealed class IntentClusterer : IIntentClusterer |
| | | 9 | | { |
| | | 10 | | /// <inheritdoc /> |
| | | 11 | | public Task<IReadOnlyList<IntentCluster>> ClusterByPatternAsync( |
| | | 12 | | IReadOnlyList<IntentHistoryRecord> records, |
| | | 13 | | CancellationToken cancellationToken = default) |
| | 3 | 14 | | { |
| | 3 | 15 | | cancellationToken.ThrowIfCancellationRequested(); |
| | | 16 | | |
| | 3 | 17 | | var groups = records |
| | 5 | 18 | | .GroupBy(r => (r.ConfidenceLevel, r.Decision)) |
| | 3 | 19 | | .Select(g => |
| | 3 | 20 | | { |
| | 3 | 21 | | var list = g.ToList(); |
| | 8 | 22 | | var ids = list.Select(x => x.Id).ToList(); |
| | 8 | 23 | | var scores = list.Select(x => x.ConfidenceScore).ToList(); |
| | 3 | 24 | | var avg = scores.Average(); |
| | 3 | 25 | | var min = scores.Min(); |
| | 3 | 26 | | var max = scores.Max(); |
| | 3 | 27 | | var id = $"{g.Key.ConfidenceLevel}_{g.Key.Decision}"; |
| | 3 | 28 | | var label = $"{g.Key.ConfidenceLevel} / {g.Key.Decision}"; |
| | 3 | 29 | | var summary = new ClusterSummary(avg, min, max); |
| | 3 | 30 | | return new IntentCluster(id, label, ids, list.Count, summary); |
| | 3 | 31 | | }) |
| | 2 | 32 | | .OrderByDescending(c => c.Count) |
| | 3 | 33 | | .ToList(); |
| | | 34 | | |
| | 3 | 35 | | return Task.FromResult<IReadOnlyList<IntentCluster>>(groups); |
| | 3 | 36 | | } |
| | | 37 | | |
| | | 38 | | /// <inheritdoc /> |
| | | 39 | | public Task<IReadOnlyList<IntentCluster>> ClusterByConfidenceScoreAsync( |
| | | 40 | | IReadOnlyList<IntentHistoryRecord> records, |
| | | 41 | | int k = 3, |
| | | 42 | | CancellationToken cancellationToken = default) |
| | 1 | 43 | | { |
| | 1 | 44 | | cancellationToken.ThrowIfCancellationRequested(); |
| | | 45 | | |
| | 1 | 46 | | if (k < 1) k = 1; |
| | 1 | 47 | | var list = records.ToList(); |
| | 1 | 48 | | if (list.Count == 0) |
| | 0 | 49 | | return Task.FromResult<IReadOnlyList<IntentCluster>>([]); |
| | | 50 | | |
| | 7 | 51 | | var scores = list.Select(r => r.ConfidenceScore).OrderBy(s => s).ToList(); |
| | 1 | 52 | | var minScore = scores[0]; |
| | 1 | 53 | | var maxScore = scores[^1]; |
| | 1 | 54 | | var range = maxScore - minScore; |
| | 1 | 55 | | var bucketWidth = range <= 0 ? 1.0 : range / k; |
| | | 56 | | |
| | 1 | 57 | | var clusters = new List<IntentCluster>(); |
| | 8 | 58 | | for (var i = 0; i < k; i++) |
| | 3 | 59 | | { |
| | 3 | 60 | | var low = minScore + i * bucketWidth; |
| | 3 | 61 | | var high = i == k - 1 ? maxScore + 0.001 : minScore + (i + 1) * bucketWidth; |
| | 12 | 62 | | var inBucket = list.Where(r => r.ConfidenceScore >= low && r.ConfidenceScore < high).ToList(); |
| | 6 | 63 | | var ids = inBucket.Select(x => x.Id).ToList(); |
| | 6 | 64 | | var bucketScores = inBucket.Select(x => x.ConfidenceScore).ToList(); |
| | 3 | 65 | | var avg = bucketScores.Count > 0 ? bucketScores.Average() : 0; |
| | 3 | 66 | | var min = bucketScores.Count > 0 ? bucketScores.Min() : 0; |
| | 3 | 67 | | var max = bucketScores.Count > 0 ? bucketScores.Max() : 0; |
| | 3 | 68 | | var clusterId = $"ScoreBucket_{i}"; |
| | 3 | 69 | | var label = $"[{low:F2}, {high:F2})"; |
| | 3 | 70 | | clusters.Add(new IntentCluster(clusterId, label, ids, inBucket.Count, new ClusterSummary(avg, min, max))); |
| | 3 | 71 | | } |
| | | 72 | | |
| | 1 | 73 | | return Task.FromResult<IReadOnlyList<IntentCluster>>(clusters); |
| | 1 | 74 | | } |
| | | 75 | | } |