| | | 1 | | using Intentum.AI.Embeddings; |
| | | 2 | | |
| | | 3 | | namespace Intentum.AI.Resilience; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Embedding provider that tries multiple providers in order, falling back to the next |
| | | 7 | | /// when one fails. Implements a simple circuit breaker: after consecutive failures, |
| | | 8 | | /// a provider is skipped for a cooldown period before being retried. |
| | | 9 | | /// </summary> |
| | | 10 | | public sealed class FallbackEmbeddingProvider : IIntentEmbeddingProvider |
| | | 11 | | { |
| | | 12 | | private readonly List<IIntentEmbeddingProvider> _providers; |
| | | 13 | | private readonly int _maxConsecutiveFailures; |
| | | 14 | | private readonly TimeSpan _cooldownPeriod; |
| | | 15 | | private readonly int[] _consecutiveFailures; |
| | | 16 | | private readonly DateTimeOffset[] _lastFailureTimes; |
| | | 17 | | |
| | 7 | 18 | | public FallbackEmbeddingProvider( |
| | 7 | 19 | | IEnumerable<IIntentEmbeddingProvider> providers, |
| | 7 | 20 | | int maxConsecutiveFailures = 3, |
| | 7 | 21 | | TimeSpan? cooldownPeriod = null) |
| | 7 | 22 | | { |
| | 7 | 23 | | _providers = (providers ?? throw new ArgumentNullException(nameof(providers))).ToList(); |
| | 6 | 24 | | if (_providers.Count == 0) |
| | 1 | 25 | | throw new ArgumentException("At least one provider is required.", nameof(providers)); |
| | | 26 | | |
| | 5 | 27 | | _maxConsecutiveFailures = maxConsecutiveFailures; |
| | 5 | 28 | | _cooldownPeriod = cooldownPeriod ?? TimeSpan.FromMinutes(1); |
| | 5 | 29 | | _consecutiveFailures = new int[_providers.Count]; |
| | 5 | 30 | | _lastFailureTimes = new DateTimeOffset[_providers.Count]; |
| | 5 | 31 | | } |
| | | 32 | | |
| | | 33 | | public IntentEmbedding Embed(string behaviorKey) |
| | 1 | 34 | | => EmbedAsync(behaviorKey, CancellationToken.None).GetAwaiter().GetResult(); |
| | | 35 | | |
| | | 36 | | public async Task<IntentEmbedding> EmbedAsync(string behaviorKey, CancellationToken cancellationToken = default) |
| | 7 | 37 | | { |
| | 7 | 38 | | Exception? lastException = null; |
| | | 39 | | |
| | 24 | 40 | | for (var i = 0; i < _providers.Count; i++) |
| | 9 | 41 | | { |
| | 9 | 42 | | if (IsCircuitOpen(i)) |
| | 0 | 43 | | continue; |
| | | 44 | | |
| | | 45 | | try |
| | 9 | 46 | | { |
| | 9 | 47 | | var result = await _providers[i].EmbedAsync(behaviorKey, cancellationToken); |
| | 4 | 48 | | _consecutiveFailures[i] = 0; |
| | 4 | 49 | | return result; |
| | | 50 | | } |
| | 5 | 51 | | catch (Exception ex) |
| | 5 | 52 | | { |
| | 5 | 53 | | _consecutiveFailures[i]++; |
| | 5 | 54 | | _lastFailureTimes[i] = DateTimeOffset.UtcNow; |
| | 5 | 55 | | lastException = ex; |
| | 5 | 56 | | } |
| | 5 | 57 | | } |
| | | 58 | | |
| | 3 | 59 | | throw new InvalidOperationException( |
| | 3 | 60 | | "All embedding providers failed.", lastException); |
| | 4 | 61 | | } |
| | | 62 | | |
| | | 63 | | private bool IsCircuitOpen(int index) |
| | 9 | 64 | | { |
| | 9 | 65 | | if (_consecutiveFailures[index] < _maxConsecutiveFailures) |
| | 8 | 66 | | return false; |
| | | 67 | | |
| | 1 | 68 | | var elapsed = DateTimeOffset.UtcNow - _lastFailureTimes[index]; |
| | 1 | 69 | | if (elapsed >= _cooldownPeriod) |
| | 1 | 70 | | { |
| | 1 | 71 | | _consecutiveFailures[index] = 0; |
| | 1 | 72 | | return false; |
| | | 73 | | } |
| | | 74 | | |
| | 0 | 75 | | return true; |
| | 9 | 76 | | } |
| | | 77 | | } |