diff --git a/docs/release-notes/breaking-changes.md b/docs/release-notes/breaking-changes.md index 5dd6135912..868d1ad02d 100644 --- a/docs/release-notes/breaking-changes.md +++ b/docs/release-notes/breaking-changes.md @@ -23,6 +23,7 @@ The following new App Configuration settings are required: |`FoundationaLLM:UserPortal:Configuration:ShowLastConversationOnStartup` | `false` | If `true`, the last conversation will be displayed when the user logs in. Otherwise, a new conversation placeholder appears on page load. | |`FoundationaLLM:UserPortal:Configuration:ShowMessageTokens` | `true` | If `true`, the number of consumed tokens on agent and user messages will appear. | |`FoundationaLLM:UserPortal:Configuration:ShowViewPrompt` | `true` | If `true`, the "View Prompt" button on agent messages will appear. | +|`FoundationaLLM:Instance:EnableResourceProvidersCache` | `false` | If `true`, the caching of resource providers will be enabled. | #### Agent Tool configuration changes diff --git a/src/dotnet/Common/Constants/Data/AppConfiguration.json b/src/dotnet/Common/Constants/Data/AppConfiguration.json index b3f32e28e0..48adfcc73a 100644 --- a/src/dotnet/Common/Constants/Data/AppConfiguration.json +++ b/src/dotnet/Common/Constants/Data/AppConfiguration.json @@ -37,6 +37,14 @@ "value": "", "content_type": "", "first_version": "0.8.0" + }, + { + "name": "EnableResourceProvidersCache", + "description": "Enable caching for resource providers.", + "secret": "", + "value": "false", + "content_type": "", + "first_version": "0.9.1" } ] }, diff --git a/src/dotnet/Common/Interfaces/IResourceProviderResourceCacheService.cs b/src/dotnet/Common/Interfaces/IResourceProviderResourceCacheService.cs new file mode 100644 index 0000000000..410ce664dd --- /dev/null +++ b/src/dotnet/Common/Interfaces/IResourceProviderResourceCacheService.cs @@ -0,0 +1,27 @@ +using FoundationaLLM.Common.Models.ResourceProviders; + +namespace FoundationaLLM.Common.Interfaces +{ + /// + /// Provides the resource caching services used by FoundationaLLM resource providers. + /// + public interface IResourceProviderResourceCacheService + { + /// + /// Tries to get a resource value identified by a resource reference from the cache. + /// + /// The type of resource value to be retrieved. + /// The used as a key in the cache. + /// The resource value to be retrieved. + /// is the resource value was found in the cache, otherwise. + bool TryGetValue(ResourceReference resourceReference, out T? resourceValue) where T: ResourceBase; + + /// + /// Sets a resource value identified by a resource reference in the cache. + /// + /// The type of resource value to be set. + /// The used as a key in the cache. + /// The resource value to be set. + void SetValue(ResourceReference resourceReference, T resourceValue) where T : ResourceBase; + } +} diff --git a/src/dotnet/Common/Models/Configuration/Instance/InstanceSettings.cs b/src/dotnet/Common/Models/Configuration/Instance/InstanceSettings.cs index 326abbffba..e13ea1d188 100644 --- a/src/dotnet/Common/Models/Configuration/Instance/InstanceSettings.cs +++ b/src/dotnet/Common/Models/Configuration/Instance/InstanceSettings.cs @@ -25,5 +25,10 @@ public class InstanceSettings /// The Regex pattern used to validate the values allowed as User Principal Name (UPN) substitutes in the X-USER-IDENTITY header. /// public string? IdentitySubstitutionUserPrincipalNamePattern { get; set; } + + /// + /// Enabling caching for resource providers. + /// + public bool EnableResourceProvidersCache { get; set; } = false; } } diff --git a/src/dotnet/Common/Services/ResourceProviders/ResourceProviderResourceCacheService.cs b/src/dotnet/Common/Services/ResourceProviders/ResourceProviderResourceCacheService.cs new file mode 100644 index 0000000000..9b69ae76b5 --- /dev/null +++ b/src/dotnet/Common/Services/ResourceProviders/ResourceProviderResourceCacheService.cs @@ -0,0 +1,75 @@ +using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models.ResourceProviders; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; + +namespace FoundationaLLM.Common.Services.ResourceProviders +{ + /// + /// Provides the resource caching services used by FoundationaLLM resource providers. + /// + /// The used to log information. + public class ResourceProviderResourceCacheService( + ILogger logger) : IResourceProviderResourceCacheService + { + private readonly ILogger _logger = logger; + + private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions + { + SizeLimit = 10000, // Limit cache size to 5000 resources. + ExpirationScanFrequency = TimeSpan.FromMinutes(5) // Scan for expired items every five minutes. + }); + private readonly MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(TimeSpan.FromMinutes(60)) // Cache entries are valid for 60 minutes. + .SetSlidingExpiration(TimeSpan.FromMinutes(30)) // Reset expiration time if accessed within 5 minutes. + .SetSize(1); // Each cache entry is a single resource. + + /// + public void SetValue(ResourceReference resourceReference, T resourceValue) where T : ResourceBase + { + try + { + _cache.Set(GetCacheKey(resourceReference), resourceValue, _cacheEntryOptions); + _logger.LogInformation("The resource {ResourceName} of type {ResourceType} has been set in the cache.", + resourceReference.Name, + resourceReference.Type); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an error setting the resource {ResourceName} of type {ResourceType} in the cache.", + resourceReference.Name, + resourceReference.Type); + } + } + + /// + public bool TryGetValue(ResourceReference resourceReference, out T? resourceValue) where T : ResourceBase + { + resourceValue = default; + + try + { + if (_cache.TryGetValue(GetCacheKey(resourceReference), out T? cachedValue) + && cachedValue != null) + { + resourceValue = cachedValue; + _logger.LogInformation("The resource {ResourceName} of type {ResourceType} has been retrieved from the cache.", + resourceReference.Name, + resourceReference.Type); + return true; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an error getting the resource {ResourceName} of type {ResourceType} from the cache.", + resourceReference.Name, + resourceReference.Type); + } + + return false; + } + + private string GetCacheKey(ResourceReference resourceReference) => + $"{resourceReference.Type}|{resourceReference.Name}"; + } +} diff --git a/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs b/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs index 79b2d81f4f..10030167dd 100644 --- a/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs +++ b/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs @@ -11,6 +11,7 @@ using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Services.Events; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Collections.Immutable; @@ -37,6 +38,8 @@ public class ResourceProviderServiceBase : IResourceProvider private readonly bool _useInternalReferencesStore; private readonly SemaphoreSlim _lock = new(1, 1); + private readonly IResourceProviderResourceCacheService? _resourceCache; + /// /// The resource reference store used by the resource provider. /// @@ -143,6 +146,9 @@ public ResourceProviderServiceBase( _eventNamespacesToSubscribe = eventNamespacesToSubscribe; _useInternalReferencesStore = useInternalReferencesStore; + if (_instanceSettings.EnableResourceProvidersCache) + _resourceCache = new ResourceProviderResourceCacheService(_logger); + _allowedResourceProviders = [_name]; _allowedResourceTypes = GetResourceTypes(); @@ -996,6 +1002,12 @@ protected async Task>> LoadResources( $"The resource reference {resourceReference.Name} is not of the expected type {typeof(T).Name}.", StatusCodes.Status400BadRequest); + + if (_resourceCache != null + && _resourceCache.TryGetValue(resourceReference, out T? cachedResource) + && cachedResource != null) + return cachedResource; + if (await _storageService.FileExistsAsync(_storageContainerName, resourceReference.Filename, default)) { var fileContent = @@ -1009,6 +1021,8 @@ protected async Task>> LoadResources( ?? throw new ResourceProviderException($"Failed to load the resource {resourceReference.Name}. Its content file might be corrupt.", StatusCodes.Status500InternalServerError); + _resourceCache?.SetValue(resourceReference, resourceObject); + return resourceObject; } catch (Exception ex) @@ -1038,6 +1052,11 @@ protected async Task>> LoadResources( ?? throw new ResourceProviderException($"Could not locate the {resourceName} resource.", StatusCodes.Status404NotFound); + if (_resourceCache != null + && _resourceCache.TryGetValue(resourceReference, out T? cachedResource) + && cachedResource != null) + return cachedResource; + if (await _storageService.FileExistsAsync(_storageContainerName, resourceReference.Filename, default)) { var fileContent = @@ -1048,6 +1067,8 @@ protected async Task>> LoadResources( ?? throw new ResourceProviderException($"Failed to load the resource {resourceReference.Name}. Its content file might be corrupt.", StatusCodes.Status400BadRequest); + _resourceCache?.SetValue(resourceReference, resourceObject); + return resourceObject; } @@ -1086,6 +1107,9 @@ await _storageService.WriteFileAsync( default); await _resourceReferenceStore!.AddResourceReference(resourceReference); + + // Add resource to cache if caching is enabled. + _resourceCache?.SetValue(resourceReference, resource); } finally { @@ -1200,6 +1224,9 @@ await _storageService.WriteFileAsync( JsonSerializer.Serialize(resource, _serializerSettings), default, default); + + // Update resource to cache if caching is enabled. + _resourceCache?.SetValue(resourceReference, resource); } finally { diff --git a/src/dotnet/Common/Templates/AppConfigurationKeys.cs b/src/dotnet/Common/Templates/AppConfigurationKeys.cs index a784ef55f7..94719ccb8f 100644 --- a/src/dotnet/Common/Templates/AppConfigurationKeys.cs +++ b/src/dotnet/Common/Templates/AppConfigurationKeys.cs @@ -41,6 +41,13 @@ public static class AppConfigurationKeys /// public const string FoundationaLLM_Instance_IdentitySubstitutionUserPrincipalNamePattern = "FoundationaLLM:Instance:IdentitySubstitutionUserPrincipalNamePattern"; + + /// + /// The app configuration key for the FoundationaLLM:Instance:EnableResourceProvidersCache setting. + /// Value description:
Enable caching for resource providers.
+ ///
+ public const string FoundationaLLM_Instance_EnableResourceProvidersCache = + "FoundationaLLM:Instance:EnableResourceProvidersCache"; #endregion diff --git a/src/dotnet/Common/Templates/appconfig.template.json b/src/dotnet/Common/Templates/appconfig.template.json index 8949092015..54fc1427d2 100644 --- a/src/dotnet/Common/Templates/appconfig.template.json +++ b/src/dotnet/Common/Templates/appconfig.template.json @@ -35,6 +35,13 @@ "content_type": "", "tags": {} }, + { + "key": "FoundationaLLM:Instance:EnableResourceProvidersCache", + "value": "false", + "label": null, + "content_type": "", + "tags": {} + }, { "key": "FoundationaLLM:Configuration:KeyVaultURI", "value": "${env:AZURE_KEY_VAULT_ENDPOINT}",