| | | 1 | | using Intentum.AI.Embeddings; |
| | | 2 | | using Intentum.Core.Behavior; |
| | | 3 | | |
| | | 4 | | namespace Intentum.AI.Similarity; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Similarity engine that applies time-based decay to embeddings. |
| | | 8 | | /// More recent events have higher influence on intent inference. |
| | | 9 | | /// When used with LlmIntentModel, time decay is applied automatically (ITimeAwareSimilarityEngine). |
| | | 10 | | /// </summary> |
| | | 11 | | public sealed class TimeDecaySimilarityEngine : ITimeAwareSimilarityEngine |
| | | 12 | | { |
| | | 13 | | private readonly TimeSpan _halfLife; |
| | | 14 | | private readonly DateTimeOffset _referenceTime; |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Creates a time decay similarity engine. |
| | | 18 | | /// </summary> |
| | | 19 | | /// <param name="halfLife">Time span after which an event's weight is halved. Defaults to 1 hour.</param> |
| | | 20 | | /// <param name="referenceTime">Reference time for decay calculation. Defaults to UtcNow.</param> |
| | 4 | 21 | | public TimeDecaySimilarityEngine( |
| | 4 | 22 | | TimeSpan? halfLife = null, |
| | 4 | 23 | | DateTimeOffset? referenceTime = null) |
| | 4 | 24 | | { |
| | 4 | 25 | | _halfLife = halfLife ?? TimeSpan.FromHours(1); |
| | 4 | 26 | | _referenceTime = referenceTime ?? DateTimeOffset.UtcNow; |
| | 4 | 27 | | } |
| | | 28 | | |
| | | 29 | | /// <summary> |
| | | 30 | | /// Calculates intent score with time decay applied. |
| | | 31 | | /// Note: This overload requires BehaviorSpace to access timestamps. |
| | | 32 | | /// </summary> |
| | | 33 | | public double CalculateIntentScoreWithTimeDecay( |
| | | 34 | | BehaviorSpace behaviorSpace, |
| | | 35 | | IReadOnlyCollection<IntentEmbedding> embeddings) |
| | 3 | 36 | | { |
| | 3 | 37 | | if (embeddings.Count == 0 || behaviorSpace.Events.Count == 0) |
| | 1 | 38 | | return 0; |
| | | 39 | | |
| | | 40 | | // Create a mapping from source (actor:action) to the most recent timestamp |
| | 2 | 41 | | var sourceToTimestamp = new Dictionary<string, DateTimeOffset>(); |
| | 12 | 42 | | foreach (var evt in behaviorSpace.Events) |
| | 3 | 43 | | { |
| | 3 | 44 | | var source = $"{evt.Actor}:{evt.Action}"; |
| | 3 | 45 | | if (!sourceToTimestamp.TryGetValue(source, out var existing) || evt.OccurredAt > existing) |
| | 3 | 46 | | { |
| | 3 | 47 | | sourceToTimestamp[source] = evt.OccurredAt; |
| | 3 | 48 | | } |
| | 3 | 49 | | } |
| | | 50 | | |
| | 2 | 51 | | var totalWeightedScore = 0.0; |
| | 2 | 52 | | var totalWeight = 0.0; |
| | | 53 | | |
| | 12 | 54 | | foreach (var embedding in embeddings) |
| | 3 | 55 | | { |
| | 3 | 56 | | if (!sourceToTimestamp.TryGetValue(embedding.Source, out var timestamp)) |
| | 0 | 57 | | continue; |
| | | 58 | | |
| | 3 | 59 | | var age = _referenceTime - timestamp; |
| | 3 | 60 | | var decayFactor = CalculateDecayFactor(age); |
| | | 61 | | |
| | 3 | 62 | | totalWeightedScore += embedding.Score * decayFactor; |
| | 3 | 63 | | totalWeight += decayFactor; |
| | 3 | 64 | | } |
| | | 65 | | |
| | 2 | 66 | | return totalWeight > 0 ? totalWeightedScore / totalWeight : 0; |
| | 3 | 67 | | } |
| | | 68 | | |
| | | 69 | | /// <summary> |
| | | 70 | | /// Standard interface implementation - uses simple average (no time decay). |
| | | 71 | | /// Use CalculateIntentScoreWithTimeDecay for time-based decay. |
| | | 72 | | /// </summary> |
| | | 73 | | public double CalculateIntentScore(IReadOnlyCollection<IntentEmbedding> embeddings) |
| | 1 | 74 | | { |
| | | 75 | | // Fallback to simple average when timestamps are not available |
| | 1 | 76 | | if (embeddings.Count == 0) |
| | 0 | 77 | | return 0; |
| | | 78 | | |
| | 3 | 79 | | return embeddings.Average(e => e.Score); |
| | 1 | 80 | | } |
| | | 81 | | |
| | | 82 | | private double CalculateDecayFactor(TimeSpan age) |
| | 3 | 83 | | { |
| | 3 | 84 | | if (age <= TimeSpan.Zero) |
| | 1 | 85 | | return 1.0; |
| | | 86 | | |
| | | 87 | | // Exponential decay: weight = 2^(-age/halfLife) |
| | 2 | 88 | | var halfLifeCount = age.TotalMilliseconds / _halfLife.TotalMilliseconds; |
| | 2 | 89 | | return Math.Pow(2, -halfLifeCount); |
| | 3 | 90 | | } |
| | | 91 | | } |