< Summary

Information
Class: Elsa.AI.Host.Services.AIToolRegistry
Assembly: Elsa.AI.Host
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.AI.Host/Services/AIToolRegistry.cs
Line coverage
88%
Covered lines: 119
Uncovered lines: 15
Coverable lines: 134
Total lines: 267
Line coverage: 88.8%
Branch coverage
84%
Covered branches: 71
Total branches: 84
Branch coverage: 84.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ListAsync(...)100%11100%
FindAsync(...)85.71%201469.23%
ResolveCachedTool(...)60%151063.63%
ResolveAndCacheTool(...)100%44100%
GetCachedDefinitions()75%4491.66%
UpdateToolTypeCache(...)100%88100%
GetToolInfos(...)100%11100%
CreateToolInfo(...)100%22100%
TryGetDefinition(...)100%11100%
IsVisible(...)85.71%1414100%
IsVisibleForAgent(...)75%44100%
IsVisibleForTenant(...)100%1010100%
IsVisibleForActor(...)75%44100%
HasRequiredPermissions(...)75%44100%
.ctor(...)100%11100%
get_Definition()100%210%
ExecuteAsync()100%11100%
Dispose()100%44100%
get_Tool()100%11100%
Dispose()50%2266.66%
get_ToolType()100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.AI.Host/Services/AIToolRegistry.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using Elsa.AI.Abstractions.Contracts;
 3using Elsa.AI.Abstractions.Models;
 4using Microsoft.Extensions.DependencyInjection;
 5
 6namespace Elsa.AI.Host.Services;
 7
 608public class AIToolRegistry(IServiceScopeFactory scopeFactory, AIToolEnablementService enablementService) : IAIToolRegis
 9{
 6010    private readonly object _definitionCacheLock = new();
 6011    private readonly ConcurrentDictionary<string, Type> _toolTypes = new(StringComparer.OrdinalIgnoreCase);
 6012    private readonly ConcurrentDictionary<string, bool> _uncacheableToolNames = new(StringComparer.OrdinalIgnoreCase);
 6013    private readonly ConcurrentDictionary<Type, bool> _uncacheableToolTypes = new();
 14    private volatile IReadOnlyCollection<AIToolDefinition>? _definitions;
 15
 16    public ValueTask<IReadOnlyCollection<AIToolDefinition>> ListAsync(AIToolQuery query, CancellationToken cancellationT
 17    {
 5718        var definitions = GetCachedDefinitions()
 3919            .Where(x => IsVisible(x, query))
 2720            .Select(x => x with { IsEnabled = enablementService.IsEnabled(x) })
 5721            .ToList();
 22
 5723        return ValueTask.FromResult<IReadOnlyCollection<AIToolDefinition>>(definitions);
 24    }
 25
 26    public ValueTask<IAITool?> FindAsync(string name, AIToolQuery query, CancellationToken cancellationToken = default)
 27    {
 2128        var scope = scopeFactory.CreateScope();
 2129        ResolvedTool? resolvedTool = null;
 30        try
 31        {
 2132            resolvedTool = ResolveCachedTool(scope.ServiceProvider, name) ?? ResolveAndCacheTool(scope.ServiceProvider, 
 2133            if (resolvedTool == null || !TryGetDefinition(resolvedTool.Tool, out var definition) || !IsVisible(definitio
 34            {
 435                resolvedTool?.Dispose();
 436                scope.Dispose();
 437                return ValueTask.FromResult<IAITool?>(null);
 38            }
 39
 1740            return ValueTask.FromResult<IAITool?>(new ScopedAITool(scope, resolvedTool.Tool, resolvedTool.DisposeTool));
 41        }
 042        catch
 43        {
 044            resolvedTool?.Dispose();
 045            scope.Dispose();
 046            throw;
 47        }
 2148    }
 49
 50    private ResolvedTool? ResolveCachedTool(IServiceProvider serviceProvider, string name)
 51    {
 2152        if (!_toolTypes.TryGetValue(name, out var toolType))
 453            return null;
 54
 55        ResolvedTool resolvedTool;
 56        try
 57        {
 1758            if (serviceProvider.GetService(toolType) is IAITool scopeOwnedTool)
 059                resolvedTool = new ResolvedTool(scopeOwnedTool, DisposeTool: false);
 1760            else if (ActivatorUtilities.CreateInstance(serviceProvider, toolType) is IAITool createdTool)
 1561                resolvedTool = new ResolvedTool(createdTool, DisposeTool: true);
 62            else
 063                return null;
 1564        }
 265        catch (InvalidOperationException)
 66        {
 267            _uncacheableToolNames[name] = true;
 268            _toolTypes.TryRemove(name, out _);
 269            return null;
 70        }
 71
 1572        if (!TryGetDefinition(resolvedTool.Tool, out var definition))
 73        {
 074            resolvedTool.Dispose();
 075            _uncacheableToolNames[name] = true;
 076            _toolTypes.TryRemove(name, out _);
 077            return null;
 78        }
 79
 1580        if (string.Equals(definition.Name, name, StringComparison.OrdinalIgnoreCase))
 1581            return resolvedTool;
 82
 083        resolvedTool.Dispose();
 084        return null;
 285    }
 86
 87    private ResolvedTool? ResolveAndCacheTool(IServiceProvider serviceProvider, string name)
 88    {
 689        var tools = serviceProvider.GetServices<IAITool>().ToList();
 690        var toolInfos = GetToolInfos(tools);
 691        UpdateToolTypeCache(toolInfos);
 1192        var tool = toolInfos.FirstOrDefault(x => string.Equals(x.Definition.Name, name, StringComparison.OrdinalIgnoreCa
 693        return tool == null ? null : new ResolvedTool(tool, DisposeTool: false);
 94    }
 95
 96    private IReadOnlyCollection<AIToolDefinition> GetCachedDefinitions()
 97    {
 5798        if (_definitions != null)
 599            return _definitions;
 100
 52101        lock (_definitionCacheLock)
 102        {
 52103            if (_definitions != null)
 0104                return _definitions;
 105
 52106            using var scope = scopeFactory.CreateScope();
 52107            var tools = scope.ServiceProvider.GetServices<IAITool>().ToList();
 52108            var toolInfos = GetToolInfos(tools);
 52109            UpdateToolTypeCache(toolInfos);
 110            // Definitions are cached by design. Runtime enablement is refreshed in ListAsync; dynamic Definition proper
 86111            _definitions = toolInfos.Select(x => x.Definition).ToList().AsReadOnly();
 52112            return _definitions;
 113        }
 52114    }
 115
 116    private void UpdateToolTypeCache(IReadOnlyCollection<ToolInfo> toolInfos)
 117    {
 58118        var namesByType = toolInfos
 39119            .Where(x => !_uncacheableToolTypes.ContainsKey(x.ToolType))
 39120            .Where(x => !string.IsNullOrWhiteSpace(x.Definition.Name))
 39121            .GroupBy(x => x.ToolType)
 155122            .ToDictionary(x => x.Key, x => x.Select(tool => tool.Definition.Name).Distinct(StringComparer.OrdinalIgnoreC
 123
 174124        foreach (var (toolType, toolNames) in namesByType)
 125        {
 29126            if (toolNames.Count != 1)
 127            {
 44128                foreach (var toolName in toolNames)
 129                {
 16130                    _uncacheableToolNames[toolName] = true;
 16131                    _toolTypes.TryRemove(toolName, out _);
 132                }
 133
 134                continue;
 135            }
 136
 23137            var name = toolNames[0];
 23138            if (_uncacheableToolNames.ContainsKey(name))
 139                continue;
 140
 21141            _toolTypes[name] = toolType;
 142        }
 58143    }
 144
 145    private IReadOnlyCollection<ToolInfo> GetToolInfos(IReadOnlyCollection<IAITool> tools)
 146    {
 58147        return tools
 58148            .Select(CreateToolInfo)
 42149            .Where(x => x != null)
 40150            .Select(x => x!)
 40151            .Where(x => !string.IsNullOrWhiteSpace(x.Definition.Name))
 58152            .ToList();
 153    }
 154
 155    private ToolInfo? CreateToolInfo(IAITool tool) =>
 42156        TryGetDefinition(tool, out var definition)
 42157            ? new ToolInfo(tool.GetType(), tool, definition)
 42158            : null;
 159
 160    private bool TryGetDefinition(IAITool tool, out AIToolDefinition definition)
 161    {
 162        try
 163        {
 76164            definition = tool.Definition;
 74165            return true;
 166        }
 2167        catch (InvalidOperationException)
 168        {
 2169            _uncacheableToolTypes[tool.GetType()] = true;
 2170            definition = default!;
 2171            return false;
 172        }
 76173    }
 174
 175    private static bool IsVisible(AIToolDefinition definition, AIToolQuery query) =>
 58176        (query.Mutability == null || definition.Mutability == query.Mutability) &&
 58177        (query.DangerLevel == null || definition.DangerLevel == query.DangerLevel) &&
 58178        IsVisibleForAgent(definition, query.Agent) &&
 58179        IsVisibleForTenant(definition, query.TenantId) &&
 58180        IsVisibleForActor(definition, query.ActorId) &&
 58181        HasRequiredPermissions(definition, query.UserPermissions);
 182
 183    private static bool IsVisibleForAgent(AIToolDefinition definition, string? agent)
 184    {
 58185        if (definition.AgentScopes.Count == 0)
 47186            return true;
 187
 11188        return !string.IsNullOrWhiteSpace(agent) &&
 11189               definition.AgentScopes.Contains(agent, StringComparer.OrdinalIgnoreCase);
 190    }
 191
 192    private static bool IsVisibleForTenant(AIToolDefinition definition, string? tenantId)
 193    {
 54194        if (tenantId != null)
 195        {
 41196            if (definition.TenantBehavior == AITenantBehavior.HostScoped)
 2197                return false;
 198
 39199            if (definition.TenantBehavior == AITenantBehavior.CrossTenantDenied)
 3200                return definition.TenantIds.Contains(tenantId, StringComparer.OrdinalIgnoreCase);
 201
 36202            return definition.TenantIds.Count == 0 || definition.TenantIds.Contains(tenantId, StringComparer.OrdinalIgno
 203        }
 204
 13205        return definition.TenantBehavior != AITenantBehavior.CrossTenantDenied && definition.TenantIds.Count == 0;
 206    }
 207
 208    private static bool IsVisibleForActor(AIToolDefinition definition, string? actorId)
 209    {
 48210        return definition.ActorIds.Count == 0 ||
 48211               (!string.IsNullOrWhiteSpace(actorId) && definition.ActorIds.Contains(actorId, StringComparer.OrdinalIgnor
 212    }
 213
 214    private static bool HasRequiredPermissions(AIToolDefinition definition, ICollection<string> userPermissions)
 215    {
 47216        if (definition.Permissions.Count == 0)
 40217            return true;
 218
 7219        var grantedPermissions = userPermissions
 5220            .Where(x => !string.IsNullOrWhiteSpace(x))
 7221            .ToHashSet(StringComparer.OrdinalIgnoreCase);
 222
 7223        return grantedPermissions.Contains(PermissionNames.All) || definition.Permissions.All(grantedPermissions.Contain
 224    }
 225
 17226    private class ScopedAITool(IServiceScope scope, IAITool inner, bool disposeInner) : IAITool
 227    {
 228        private bool _disposed;
 229
 0230        public AIToolDefinition Definition => inner.Definition;
 231
 232        public async ValueTask<AIToolResult> ExecuteAsync(AIToolExecutionContext context, CancellationToken cancellation
 233        {
 234            try
 235            {
 13236                return await inner.ExecuteAsync(context, cancellationToken);
 237            }
 238            finally
 239            {
 13240                Dispose();
 241            }
 12242        }
 243
 244        public void Dispose()
 245        {
 30246            if (_disposed)
 13247                return;
 248
 17249            _disposed = true;
 17250            if (disposeInner)
 15251                inner.Dispose();
 252
 17253            scope.Dispose();
 17254        }
 255    }
 256
 89257    private record ResolvedTool(IAITool Tool, bool DisposeTool)
 258    {
 259        public void Dispose()
 260        {
 2261            if (DisposeTool)
 0262                Tool.Dispose();
 2263        }
 264    }
 265
 279266    private record ToolInfo(Type ToolType, IAITool Tool, AIToolDefinition Definition);
 267}