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}",