< Summary

Information
Class: Elsa.AI.Host.Context.AIContextResolver
Assembly: Elsa.AI.Host
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.AI.Host/Context/AIContextResolver.cs
Line coverage
100%
Covered lines: 59
Uncovered lines: 0
Coverable lines: 59
Total lines: 104
Line coverage: 100%
Branch coverage
95%
Covered branches: 21
Total branches: 22
Branch coverage: 95.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
ResolveAsync()100%44100%
Redact(...)100%11100%
RedactObject(...)100%44100%
RedactNode(...)90%1010100%
RedactText(...)100%22100%
IsSensitiveKey(...)100%11100%
BuildProviders(...)100%11100%
SelectProvider(...)100%22100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.AI.Host/Context/AIContextResolver.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2using Elsa.AI.Abstractions.Contracts;
 3using Elsa.AI.Abstractions.Models;
 4using Microsoft.Extensions.DependencyInjection;
 5using Microsoft.Extensions.Logging;
 6
 7namespace Elsa.AI.Host.Context;
 8
 499public class AIContextResolver(IServiceScopeFactory scopeFactory, ILogger<AIContextResolver> logger)
 10{
 211    private static readonly string[] SensitiveKeyFragments = ["secret", "token", "password", "apikey", "api_key", "api-k
 212    private static readonly Regex[] SensitiveValuePatterns =
 213    [
 214        new(@"\bBearer\s+[A-Za-z0-9._~+/\-=]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.C
 215        new(@"\b(?:api[_-]?key|token|secret|password)\s*[:=]\s*['""]?[A-Za-z0-9._~+/\-=]{8,}['""]?", RegexOptions.Ignore
 216    ];
 17    private const string Redacted = "[redacted]";
 18
 19    public async ValueTask<IReadOnlyCollection<AIResolvedContext>> ResolveAsync(AIChatRequest request, CancellationToken
 20    {
 4621        using var scope = scopeFactory.CreateScope();
 4622        var providers = BuildProviders(scope.ServiceProvider.GetServices<IAIContextProvider>());
 4623        var resolved = new List<AIResolvedContext>();
 4624        var attachmentsWithProviders = request.Attachments
 1725            .Select(attachment => new
 1726            {
 1727                Attachment = attachment,
 1728                Provider = providers.GetValueOrDefault(attachment.Kind)
 1729            })
 6330            .Where(x => x.Provider != null);
 31
 12332        foreach (var item in attachmentsWithProviders)
 33        {
 1634            if (item.Provider is IPlaceholderAIContextProvider)
 135                logger.LogWarning(
 136                    "AI context kind {ContextKind} is using placeholder provider {ProviderType}. Replace it before produ
 137                    item.Attachment.Kind,
 138                    item.Provider.GetType().Name);
 39
 1640            var context = await item.Provider!.ResolveAsync(new AIContextResolutionRequest
 1641            {
 1642                Attachment = item.Attachment,
 1643                TenantId = request.TenantId,
 1644                UserId = request.UserId
 1645            }, cancellationToken);
 46
 1547            resolved.Add(Redact(context));
 48        }
 49
 4550        return resolved;
 4551    }
 52
 53    private static AIResolvedContext Redact(AIResolvedContext context) =>
 1554        context with
 1555        {
 1556            Summary = RedactText(context.Summary),
 1557            Data = RedactObject(context.Data),
 1558            Metadata = RedactObject(context.Metadata)
 1559        };
 60
 61    private static JsonObject RedactObject(JsonObject source)
 62    {
 3163        var redacted = new JsonObject();
 64
 9665        foreach (var property in source)
 1766            redacted[property.Key] = IsSensitiveKey(property.Key) ? Redacted : RedactNode(property.Value);
 67
 3168        return redacted;
 69    }
 70
 71    private static JsonNode? RedactNode(JsonNode? node) =>
 1772        node switch
 1773        {
 174            JsonObject jsonObject => RedactObject(jsonObject),
 175            JsonArray jsonArray => new JsonArray(jsonArray.Select(RedactNode).ToArray()),
 2676            JsonValue jsonValue when jsonValue.TryGetValue<string>(out var value) => JsonValue.Create(RedactText(value))
 277            JsonValue jsonValue => jsonValue.DeepClone(),
 178            _ => node?.DeepClone()
 1779        };
 80
 81    private static string RedactText(string text)
 82    {
 2783        if (string.IsNullOrWhiteSpace(text))
 184            return text;
 85
 7886        return SensitiveValuePatterns.Aggregate(text, (current, pattern) => pattern.Replace(current, Redacted));
 87    }
 88
 89    private static bool IsSensitiveKey(string key) =>
 15290        SensitiveKeyFragments.Any(fragment => key.Contains(fragment, StringComparison.OrdinalIgnoreCase));
 91
 92    private static Dictionary<string, IAIContextProvider> BuildProviders(IEnumerable<IAIContextProvider> providers)
 93    {
 4694        return providers
 9395            .GroupBy(x => x.Kind, StringComparer.OrdinalIgnoreCase)
 13796            .ToDictionary(x => x.Key, SelectProvider, StringComparer.OrdinalIgnoreCase);
 97    }
 98
 99    private static IAIContextProvider SelectProvider(IEnumerable<IAIContextProvider> providers)
 100    {
 91101        var providerList = providers.ToList();
 183102        return providerList.LastOrDefault(x => x is not IPlaceholderAIContextProvider) ?? providerList.Last();
 103    }
 104}