diff --git a/docs/deployment/app-configuration-values.md b/docs/deployment/app-configuration-values.md index ffc59c3d2d..a98302cd58 100644 --- a/docs/deployment/app-configuration-values.md +++ b/docs/deployment/app-configuration-values.md @@ -6,6 +6,7 @@ FoundationaLLM uses Azure App Configuration to store configuration values, Key V | Key | Default Value | Description | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FoundationaLLM:Instance:Id` | Generated GUID | The value should be a GUID represents a unique instance of the FoundationaLLM instance. | | `FoundationaLLM:AgentHub:AgentMetadata:StorageContainer` | agents | | | `FoundationaLLM:AgentHub:StorageManager:BlobStorage:ConnectionString` | Key Vault secret name: `foundationallm-agenthub-storagemanager-blobstorage-connectionstring` | This is a Key Vault reference. | | `FoundationaLLM:APIs:AgentFactoryAPI:APIKey` | Key Vault secret name: `foundationallm-apis-agentfactoryapi-apikey` | This is a Key Vault reference. | diff --git a/src/FoundationaLLM.sln b/src/FoundationaLLM.sln index 0406170311..29c5943860 100644 --- a/src/FoundationaLLM.sln +++ b/src/FoundationaLLM.sln @@ -81,7 +81,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Management", "dotnet\Manage EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagementAPI", "dotnet\ManagementAPI\ManagementAPI.csproj", "{2D54392A-8D86-4F54-9993-FB3B6C4C090E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernel", "dotnet\SemanticKernel\SemanticKernel.csproj", "{CDB843FE-108B-435A-BF17-68052C64F500}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel", "dotnet\SemanticKernel\SemanticKernel.csproj", "{CDB843FE-108B-435A-BF17-68052C64F500}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agent", "dotnet\Agent\Agent.csproj", "{9BE97AEC-032C-454B-BDAA-29418A769237}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -200,6 +202,10 @@ Global {CDB843FE-108B-435A-BF17-68052C64F500}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDB843FE-108B-435A-BF17-68052C64F500}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDB843FE-108B-435A-BF17-68052C64F500}.Release|Any CPU.Build.0 = Release|Any CPU + {9BE97AEC-032C-454B-BDAA-29418A769237}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BE97AEC-032C-454B-BDAA-29418A769237}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BE97AEC-032C-454B-BDAA-29418A769237}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BE97AEC-032C-454B-BDAA-29418A769237}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -239,6 +245,7 @@ Global {46FB5F1B-57C6-4CA3-B626-887DF6D806DD} = {B6DC1190-2873-44A3-85B3-63D7BDE99231} {2D54392A-8D86-4F54-9993-FB3B6C4C090E} = {B6DC1190-2873-44A3-85B3-63D7BDE99231} {CDB843FE-108B-435A-BF17-68052C64F500} = {B6DC1190-2873-44A3-85B3-63D7BDE99231} + {9BE97AEC-032C-454B-BDAA-29418A769237} = {B6DC1190-2873-44A3-85B3-63D7BDE99231} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FF5DE858-4B85-4EE8-8A6D-46E8E4FBA078} diff --git a/src/dotnet/Agent/Agent.csproj b/src/dotnet/Agent/Agent.csproj new file mode 100644 index 0000000000..831058821e --- /dev/null +++ b/src/dotnet/Agent/Agent.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + FoundationaLLM.Agent + FoundationaLLM.Agent + True + + + + + + + + + + + + diff --git a/src/dotnet/Agent/Models/Metadata/AgentBase.cs b/src/dotnet/Agent/Models/Metadata/AgentBase.cs new file mode 100644 index 0000000000..2df634571f --- /dev/null +++ b/src/dotnet/Agent/Models/Metadata/AgentBase.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using FoundationaLLM.Common.Models.Metadata; +using FoundationaLLM.Common.Models.ResourceProvider; +using Newtonsoft.Json; + +namespace FoundationaLLM.Agent.Models.Metadata +{ + /// + /// Base agent metadata model. + /// + public class AgentBase : ResourceBase + { + /// + /// The agent's language model configuration. + /// + [JsonProperty("language_model")] + public LanguageModel? LanguageModel { get; set; } + /// + /// Indicates whether sessions are enabled for the agent. + /// + [JsonProperty("sessions_enabled")] + public bool SessionsEnabled { get; set; } + /// + /// The agent's conversation history configuration. + /// + [JsonProperty("conversation_history")] + public ConversationHistory? ConversationHistory { get; set; } + /// + /// The agent's Gatekeeper configuration. + /// + [JsonProperty("gatekeeper")] + public Gatekeeper? Gatekeeper { get; set; } + /// + /// The agent's LLM orchestrator type. + /// + [JsonProperty("orchestrator")] + public string? Orchestrator { get; set; } + /// + /// The agent's prompt. + /// + [JsonProperty("prompt")] + public string? Prompt { get; set; } + } + + /// + /// Agent conversation history settings. + /// + public class ConversationHistory + { + /// + /// Indicates whether the conversation history is enabled. + /// + [JsonProperty("enabled")] + public bool Enabled { get; set; } + /// + /// The maximum number of turns to store in the conversation history. + /// + [JsonProperty("max_history")] + public int MaxHistory { get; set; } + } + + /// + /// Agent Gatekeeper settings. + /// + public class Gatekeeper + { + /// + /// Indicates whether to abide by or override the system settings for the Gatekeeper. + /// + [JsonProperty("use_system_setting")] + public bool UseSystemSetting { get; set; } + /// + /// If is false, provides Gatekeeper feature selection. + /// + [JsonProperty("options")] + public string[]? Options { get; set; } + } + +} diff --git a/src/dotnet/Agent/Models/Metadata/KnowledgeManagementAgent.cs b/src/dotnet/Agent/Models/Metadata/KnowledgeManagementAgent.cs new file mode 100644 index 0000000000..8a50547803 --- /dev/null +++ b/src/dotnet/Agent/Models/Metadata/KnowledgeManagementAgent.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FoundationaLLM.Agent.Models.Metadata +{ + /// + /// The Knowledge Management agent metadata model. + /// + public class KnowledgeManagementAgent : AgentBase + { + /// + /// The vectorization indexing profile resource path. + /// + [JsonProperty("indexing_profile")] + public string? IndexingProfile { get; set; } + /// + /// The vectorization embedding profile resource path. + /// + [JsonProperty("embedding_profile")] + public string? EmbeddingProfile { get; set; } + + /// + /// Set default property values. + /// + public KnowledgeManagementAgent() => + Type = Common.Constants.AgentTypes.KnowledgeManagement; + } +} diff --git a/src/dotnet/Agent/Models/Resources/AgentReference.cs b/src/dotnet/Agent/Models/Resources/AgentReference.cs new file mode 100644 index 0000000000..3335feb037 --- /dev/null +++ b/src/dotnet/Agent/Models/Resources/AgentReference.cs @@ -0,0 +1,37 @@ +using FoundationaLLM.Agent.Models.Metadata; +using FoundationaLLM.Common.Constants; +using FoundationaLLM.Common.Exceptions; +using Newtonsoft.Json; + +namespace FoundationaLLM.Agent.Models.Resources +{ + /// + /// Provides details about an agent. + /// + public class AgentReference + { + /// + /// The name of the agent. + /// + public required string Name { get; set; } + /// + /// The filename of the agent. + /// + public required string Filename { get; set; } + /// + /// The type of the agent. + /// + public required string Type { get; set; } + + /// + /// The object type of the agent. + /// + [JsonIgnore] + public Type AgentType => + Type switch + { + AgentTypes.KnowledgeManagement => typeof(KnowledgeManagementAgent), + _ => throw new ResourceProviderException($"The agent type {Type} is not supported.") + }; + } +} diff --git a/src/dotnet/Agent/Models/Resources/AgentReferenceStore.cs b/src/dotnet/Agent/Models/Resources/AgentReferenceStore.cs new file mode 100644 index 0000000000..c8e1e46170 --- /dev/null +++ b/src/dotnet/Agent/Models/Resources/AgentReferenceStore.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FoundationaLLM.Agent.Models.Resources +{ + /// + /// Models the content of the agent reference store managed by the FoundationaLLM.Agent resource provider. + /// + public class AgentReferenceStore + { + /// + /// The list of all agents registered in the system. + /// + public required List AgentReferences { get; set; } + + /// + /// Creates a string-based dictionary of values from the current object. + /// + /// The string-based dictionary of values from the current object. + public Dictionary ToDictionary() => + AgentReferences.ToDictionary(ar => ar.Name); + + /// + /// Creates a new instance of the from a dictionary. + /// + /// A string-based dictionary of values. + /// The object created from the dictionary. + public static AgentReferenceStore FromDictionary(Dictionary dictionary) => + new AgentReferenceStore + { + AgentReferences = dictionary.Values.ToList() + }; + } +} diff --git a/src/dotnet/Agent/ResourceProviders/AgentResourceProviderService.cs b/src/dotnet/Agent/ResourceProviders/AgentResourceProviderService.cs new file mode 100644 index 0000000000..5188cc69ae --- /dev/null +++ b/src/dotnet/Agent/ResourceProviders/AgentResourceProviderService.cs @@ -0,0 +1,235 @@ +using System.Collections.Concurrent; +using System.Text; +using FoundationaLLM.Agent.Models.Metadata; +using FoundationaLLM.Agent.Models.Resources; +using FoundationaLLM.Common.Constants; +using FoundationaLLM.Common.Exceptions; +using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models.Configuration.Instance; +using FoundationaLLM.Common.Services.ResourceProviders; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace FoundationaLLM.Agent.ResourceProviders +{ + /// + /// Implements the FoundationaLLM.Agent resource provider. + /// + public class AgentResourceProviderService( + IOptions instanceOptions, + [FromKeyedServices(DependencyInjectionKeys.FoundationaLLM_Agent_ResourceProviderService)] IStorageService storageService, + ILogger logger) + : ResourceProviderServiceBase( + instanceOptions.Value, + storageService, + logger) + { + private readonly JsonSerializerSettings _serializerSettings = new() + { + TypeNameHandling = TypeNameHandling.Auto, + Formatting = Formatting.Indented + }; + private ConcurrentDictionary _agentReferences = []; + + private const string AGENT_REFERENCES_FILE_NAME = "_agent-references.json"; + private const string AGENT_REFERENCES_FILE_PATH = $"/{ResourceProviderNames.FoundationaLLM_Agent}/_agent-references.json"; + + /// + protected override string _name => ResourceProviderNames.FoundationaLLM_Agent; + + /// + protected override Dictionary _resourceTypes => + new() + { + { + AgentResourceTypeNames.Agents, + new ResourceTypeDescriptor(AgentResourceTypeNames.Agents) + }, + { + AgentResourceTypeNames.AgentReferences, + new ResourceTypeDescriptor(AgentResourceTypeNames.AgentReferences) + } + }; + + /// + protected override async Task InitializeInternal() + { + _logger.LogInformation("Starting to initialize the {ResourceProvider} resource provider...", _name); + + if (await _storageService.FileExistsAsync(_storageContainerName, AGENT_REFERENCES_FILE_PATH, default)) + { + var fileContent = await _storageService.ReadFileAsync(_storageContainerName, AGENT_REFERENCES_FILE_PATH, default); + var agentReferenceStore = JsonConvert.DeserializeObject( + Encoding.UTF8.GetString(fileContent.ToArray())); + + _agentReferences = new ConcurrentDictionary( + agentReferenceStore!.ToDictionary()); + } + else + { + await _storageService.WriteFileAsync( + _storageContainerName, + AGENT_REFERENCES_FILE_PATH, + JsonConvert.SerializeObject(new AgentReferenceStore { AgentReferences = [] }), + default, + default); + } + + _logger.LogInformation("The {ResourceProvider} resource provider was successfully initialized.", _name); + } + + /// + protected override async Task GetResourcesAsyncInternal(List instances) => + instances[0].ResourceType switch + { + AgentResourceTypeNames.Agents => await LoadAndSerializeAgents(instances[0]), + _ => throw new ResourceProviderException($"The resource type {instances[0].ResourceType} is not supported by the {_name} resource manager.") + }; + + /// + protected override async Task UpsertResourceAsync(List instances, string serializedResource) + { + switch (instances[0].ResourceType) + { + case AgentResourceTypeNames.Agents: + await UpdateAgent(instances, serializedResource); + break; + default: + throw new ResourceProviderException($"The resource type {instances[0].ResourceType} is not supported by the {_name} resource manager."); + } + } + + private async Task LoadAndSerializeAgents(ResourceTypeInstance instance) + { + if (instance.ResourceId == null) + { + var serializedAgents = new List(); + + foreach (var agentReference in _agentReferences.Values) + { + var agent = await LoadAgent(agentReference); + serializedAgents.Add( + JsonConvert.SerializeObject(agent, agentReference.AgentType, _serializerSettings)); + } + + return $"[{string.Join(",", [.. serializedAgents])}]"; + } + else + { + if (!_agentReferences.TryGetValue(instance.ResourceId, out var agentReference)) + throw new ResourceProviderException($"Could not locate the {instance.ResourceId} agent resource."); + + var agent = await LoadAgent(agentReference); + return JsonConvert.SerializeObject(agent, agentReference.AgentType, _serializerSettings); + } + } + + private async Task LoadAgent(AgentReference agentReference) + { + if (await _storageService.FileExistsAsync(_storageContainerName, agentReference.Filename, default)) + { + var fileContent = await _storageService.ReadFileAsync(_storageContainerName, agentReference.Filename, default); + return JsonConvert.DeserializeObject( + Encoding.UTF8.GetString(fileContent.ToArray()), + agentReference.AgentType, + _serializerSettings) as AgentBase + ?? throw new ResourceProviderException($"Failed to load the agent {agentReference.Name}."); + } + + throw new ResourceProviderException($"Could not locate the {agentReference.Name} agent resource."); + } + + private async Task UpdateAgent(List instances, string serializedAgent) + { + var agentBase = JsonConvert.DeserializeObject(serializedAgent) + ?? throw new ResourceProviderException("The object definition is invalid."); + + if (instances[0].ResourceId != agentBase.Name) + throw new ResourceProviderException("The resource path does not match the object definition (name mismatch)."); + + var agentReference = new AgentReference + { + Name = agentBase.Name!, + Type = agentBase.Type!, + Filename = $"/{_name}/{agentBase.Name}.json" + }; + + var agent = JsonConvert.DeserializeObject(serializedAgent, agentReference.AgentType, _serializerSettings); + (agent as AgentBase)!.ObjectId = GetObjectId(instances); + + await _storageService.WriteFileAsync( + _storageContainerName, + agentReference.Filename, + JsonConvert.SerializeObject(agent, agentReference.AgentType, _serializerSettings), + default, + default); + + _agentReferences[agentReference.Name] = agentReference; + + await _storageService.WriteFileAsync( + _storageContainerName, + AGENT_REFERENCES_FILE_PATH, + JsonConvert.SerializeObject(AgentReferenceStore.FromDictionary(_agentReferences.ToDictionary())), + default, + default); + } + + + + + + + + + /// + protected override async Task GetResourceAsyncInternal(List instances) where T: class => + instances[0].ResourceType switch + { + AgentResourceTypeNames.AgentReferences => await GetAgentAsync(instances), + _ => throw new ResourceProviderException($"The resource type {instances[0].ResourceType} is not supported by the {_name} resource manager.") + }; + + /// + protected override async Task> GetResourcesAsyncInternal(List instances) where T : class => + instances[0].ResourceType switch + { + AgentResourceTypeNames.AgentReferences => await GetAgentsAsync(instances), + _ => throw new ResourceProviderException($"The resource type {instances[0].ResourceType} is not supported by the {_name} resource manager.") + }; + + + private async Task> GetAgentsAsync(List instances) where T : class + { + if (typeof(T) != typeof(AgentReference)) + throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({instances[0].ResourceType})."); + + var agentReferences = _agentReferences.Values.Cast().ToList(); + foreach (var agentReference in agentReferences) + { + var agent = await LoadAgent(agentReference); + } + + return agentReferences.Cast().ToList(); + } + + private async Task GetAgentAsync(List instances) where T : class + { + if (instances.Count != 1) + throw new ResourceProviderException($"Invalid resource path"); + + if (typeof(T) != typeof(AgentReference)) + throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({instances[0].ResourceType})."); + + _agentReferences.TryGetValue(instances[0].ResourceId!, out var agentReference); + if (agentReference != null) + { + return agentReference as T ?? throw new ResourceProviderException( + $"The resource {instances[0].ResourceId!} of type {instances[0].ResourceType} was not found."); + } + throw new ResourceProviderException( + $"The resource {instances[0].ResourceId!} of type {instances[0].ResourceType} was not found."); + } + } +} diff --git a/src/dotnet/Agent/ResourceProviders/AgentResourceTypeNames.cs b/src/dotnet/Agent/ResourceProviders/AgentResourceTypeNames.cs new file mode 100644 index 0000000000..627723b082 --- /dev/null +++ b/src/dotnet/Agent/ResourceProviders/AgentResourceTypeNames.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FoundationaLLM.Agent.ResourceProviders +{ + /// + /// Contains constants of the names of the resource types managed by the FoundationaLLM.Agent resource manager. + /// + public class AgentResourceTypeNames + { + /// + /// Agent references. + /// + public const string AgentReferences = "agentreferences"; + /// + /// Agents. + /// + public const string Agents = "agents"; + } +} diff --git a/src/dotnet/AgentFactory/Models/Orchestration/Metadata/DataSourceBase.cs b/src/dotnet/AgentFactory/Models/Orchestration/Metadata/DataSourceBase.cs index e7327b8335..df9145e8b1 100644 --- a/src/dotnet/AgentFactory/Models/Orchestration/Metadata/DataSourceBase.cs +++ b/src/dotnet/AgentFactory/Models/Orchestration/Metadata/DataSourceBase.cs @@ -9,7 +9,7 @@ namespace FoundationaLLM.AgentFactory.Core.Models.Orchestration.Metadata public class DataSourceBase : MetadataBase { /// - /// Discriptor for the type of data in the data source. + /// Descriptor for the type of data in the data source. /// /// Survey data for a CSV file that contains survey results. [JsonProperty("data_description")] diff --git a/src/dotnet/AgentFactoryAPI/Program.cs b/src/dotnet/AgentFactoryAPI/Program.cs index 137d913187..6837f832f4 100644 --- a/src/dotnet/AgentFactoryAPI/Program.cs +++ b/src/dotnet/AgentFactoryAPI/Program.cs @@ -12,6 +12,7 @@ using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Middleware; using FoundationaLLM.Common.Models.Configuration.API; +using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.Context; using FoundationaLLM.Common.OpenAPI; using FoundationaLLM.Common.Services; @@ -72,6 +73,8 @@ public static void Main(string[] args) builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_APIs_AgentFactoryAPI)); builder.Services.AddTransient(); + builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_Instance)); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_APIs_SemanticKernelAPI)); diff --git a/src/dotnet/Common/Constants/AgentTypes.cs b/src/dotnet/Common/Constants/AgentTypes.cs new file mode 100644 index 0000000000..da40275c2d --- /dev/null +++ b/src/dotnet/Common/Constants/AgentTypes.cs @@ -0,0 +1,17 @@ +namespace FoundationaLLM.Common.Constants +{ + /// + /// Contains constants for the types of agents. + /// + public class AgentTypes + { + /// + /// Knowledge Management agents are best for Q&A, summarization, and reasoning over textual data. + /// + public const string KnowledgeManagement = "knowledge-management"; + /// + /// Analytic agents are best for querying, analyzing, calculating, and reporting on tabular data. + /// + public const string Analytic = "analytic"; + } +} diff --git a/src/dotnet/Common/Constants/AppConfigurationKeys.cs b/src/dotnet/Common/Constants/AppConfigurationKeys.cs index 31f306f876..d54373363d 100644 --- a/src/dotnet/Common/Constants/AppConfigurationKeys.cs +++ b/src/dotnet/Common/Constants/AppConfigurationKeys.cs @@ -12,6 +12,10 @@ namespace FoundationaLLM.Common.Constants /// public static class AppConfigurationKeys { + /// + /// The key for the FoundationaLLM:Instance:Id app configuration setting. + /// + public const string FoundationaLLM_Instance_Id = "FoundationaLLM:Instance:Id"; /// /// The key for the FoundationaLLM:AgentHub:AgentMetadata:StorageContainer app configuration setting. /// @@ -660,6 +664,10 @@ public static class AppConfigurationKeys /// public static class AppConfigurationKeyFilters { + /// + /// The key filter for the FoundationaLLM:Instance:* app configuration settings. + /// + public const string FoundationaLLM_Instance = "FoundationaLLM:Instance:*"; /// /// The key filter for the FoundationaLLM:Branding:* app configuration settings. /// @@ -720,6 +728,10 @@ public static class AppConfigurationKeyFilters /// The key filter for the FoundationaLLM:Vectorization:* app configuration settings. /// public const string FoundationaLLM_Vectorization = "FoundationaLLM:Vectorization:*"; + /// + /// The key filter for the FoundationaLLM:Agent:* app configuration settings. + /// + public const string FoundationaLLM_Agent = "FoundationaLLM:Agent:*"; } /// @@ -727,6 +739,10 @@ public static class AppConfigurationKeyFilters /// public static class AppConfigurationKeySections { + /// + /// The key section for the FoundationaLLM:Instance app configuration settings. + /// + public const string FoundationaLLM_Instance = "FoundationaLLM:Instance"; /// /// The key section for the FoundationaLLM:Branding app configuration settings. /// @@ -817,10 +833,6 @@ public static class AppConfigurationKeySections /// public const string FoundationaLLM_Vectorization_StateService = "FoundationaLLM:Vectorization:StateService:Storage"; /// - /// The key section for the FoundationaLLM:Vectorization:ResourceProviderService:Storage app configuration settings. - /// - public const string FoundationaLLM_Vectorization_ResourceProviderService_Storage = "FoundationaLLM:Vectorization:ResourceProviderService:Storage"; - /// /// The key section for the FoundationaLLM:Vectorization:ContentSources app configuration settings. /// public const string FoundationaLLM_Vectorization_ContentSources = "FoundationaLLM:Vectorization:ContentSources"; @@ -833,5 +845,19 @@ public static class AppConfigurationKeySections /// The key section for the FoundationaLLM:Vectorization:AzureAISearchIndexingService app configuration settings. /// public const string FoundationaLLM_Vectorization_AzureAISearchIndexingService = "FoundationaLLM:Vectorization:AzureAISearchIndexingService"; + + #region Resource providers + + /// + /// The key section for the FoundationaLLM:Vectorization:ResourceProviderService:Storage app configuration settings. + /// + public const string FoundationaLLM_Vectorization_ResourceProviderService_Storage = "FoundationaLLM:Vectorization:ResourceProviderService:Storage"; + + /// + /// The key section for the FoundationaLLM:Agent:ResourceProviderService:Storage app configuration settings. + /// + public const string FoundationaLLM_Agent_ResourceProviderService_Storage = "FoundationaLLM:Agent:ResourceProviderService:Storage"; + + #endregion } } diff --git a/src/dotnet/Common/Constants/DependencyInjectionKeys.cs b/src/dotnet/Common/Constants/DependencyInjectionKeys.cs index f79f769430..542b938c16 100644 --- a/src/dotnet/Common/Constants/DependencyInjectionKeys.cs +++ b/src/dotnet/Common/Constants/DependencyInjectionKeys.cs @@ -50,5 +50,9 @@ public static class DependencyInjectionKeys /// The dependency injection key for the vectorization steps configuration section. /// public const string FoundationaLLM_Vectorization_Steps = "FoundationaLLM:Vectorization:Steps"; + /// + /// The dependency injection key for the FoundationaLLM.Agent resource provider. + /// + public const string FoundationaLLM_Agent_ResourceProviderService = "FoundationaLLM:Agent:ResourceProviderService"; } } diff --git a/src/dotnet/Common/Constants/ResourceProviderNames.cs b/src/dotnet/Common/Constants/ResourceProviderNames.cs index e531956f91..d20e538bcc 100644 --- a/src/dotnet/Common/Constants/ResourceProviderNames.cs +++ b/src/dotnet/Common/Constants/ResourceProviderNames.cs @@ -15,5 +15,13 @@ public static class ResourceProviderNames /// The name of the FoundationaLLM.Vectorization resource provider. /// public const string FoundationaLLM_Vectorization = "FoundationaLLM.Vectorization"; + /// + /// The name of the FoundationaLLM.Agent resource provider. + /// + public const string FoundationaLLM_Agent = "FoundationaLLM.Agent"; + /// + /// The name of the FoundationaLLM.Configuration resource provider. + /// + public const string FoundationaLLM_Configuration = "FoundationaLLM.Configuration"; } } diff --git a/src/dotnet/Common/Interfaces/ICallContext.cs b/src/dotnet/Common/Interfaces/ICallContext.cs index 0ce44e2e64..31eda9a1dc 100644 --- a/src/dotnet/Common/Interfaces/ICallContext.cs +++ b/src/dotnet/Common/Interfaces/ICallContext.cs @@ -31,5 +31,9 @@ public interface ICallContext /// from one or more services. /// UnifiedUserIdentity? CurrentUserIdentity { get; set; } + /// + /// The unique identifier of the current FoundationaLLM deployment instance. + /// + string? InstanceId { get; set; } } } diff --git a/src/dotnet/Common/Interfaces/IResourceProviderService.cs b/src/dotnet/Common/Interfaces/IResourceProviderService.cs index d9d285ec2f..a953cfcb25 100644 --- a/src/dotnet/Common/Interfaces/IResourceProviderService.cs +++ b/src/dotnet/Common/Interfaces/IResourceProviderService.cs @@ -39,6 +39,13 @@ public interface IResourceProviderService /// The of resources corresponding to the specified logical path. IList GetResources(string resourcePath) where T : class; + /// + /// Gets the resources based on the logical path of the resource type. + /// + /// The logical path of the resource type. + /// The serialized form of resources corresponding to the specified logical path. + Task GetResourcesAsync(string resourcePath); + /// /// Gets a resource based on its logical path. /// @@ -79,6 +86,14 @@ public interface IResourceProviderService /// The instance of the resource being created or updated. void UpsertResource(string resourcePath, T resource) where T : class; + /// + /// Creates or updates a resource based on its logical path. + /// + /// The logical path of the resource. + /// The serialized instance of the resource being created or updated. + /// + Task UpsertResourceAsync(string resourcePath, string serializedResource); + /// /// Deletes a resource based on its logical path. /// @@ -93,5 +108,12 @@ public interface IResourceProviderService /// The type of the resource. /// The logical path of the resource. void DeleteResource(string resourcePath) where T : class; + + /// + /// Deletes a resource based on its logical path. + /// + /// The logical path of the resource. + /// + Task DeleteResourceAsync(string resourcePath); } } diff --git a/src/dotnet/Common/Interfaces/IStorageService.cs b/src/dotnet/Common/Interfaces/IStorageService.cs index becfe899c7..0cd1819ae9 100644 --- a/src/dotnet/Common/Interfaces/IStorageService.cs +++ b/src/dotnet/Common/Interfaces/IStorageService.cs @@ -11,6 +11,11 @@ namespace FoundationaLLM.Common.Interfaces /// public interface IStorageService { + /// + /// The optional instance name of the storage service. + /// + string? InstanceName { get; set; } + /// /// Reads the binary content of a specified file from the storage. /// @@ -26,9 +31,10 @@ public interface IStorageService /// The name of the container where the file is located. /// The path of the file to read. /// The binary content written to the file. + /// An optional content type. /// The cancellation token that signals that operations should be cancelled. /// - Task WriteFileAsync(string containerName, string filePath, Stream fileContent, CancellationToken cancellationToken); + Task WriteFileAsync(string containerName, string filePath, Stream fileContent, string? contentType, CancellationToken cancellationToken); /// /// Writes the string content to a specified file from the storage. @@ -36,9 +42,10 @@ public interface IStorageService /// The name of the container where the file is located. /// The path of the file to read. /// The string content written to the file. + /// An optional content type. /// The cancellation token that signals that operations should be cancelled. /// - Task WriteFileAsync(string containerName, string filePath, string fileContent, CancellationToken cancellationToken); + Task WriteFileAsync(string containerName, string filePath, string fileContent, string? contentType, CancellationToken cancellationToken); /// /// Checks if a file exists on the storage. diff --git a/src/dotnet/Common/Middleware/CallContextMiddleware.cs b/src/dotnet/Common/Middleware/CallContextMiddleware.cs index 8fa8a0fcf1..dfcd0f91d7 100644 --- a/src/dotnet/Common/Middleware/CallContextMiddleware.cs +++ b/src/dotnet/Common/Middleware/CallContextMiddleware.cs @@ -1,8 +1,10 @@ using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models.Configuration.Instance; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using FoundationaLLM.Common.Models.Metadata; +using Microsoft.Extensions.Options; namespace FoundationaLLM.Common.Middleware { @@ -28,8 +30,13 @@ public CallContextMiddleware(RequestDelegate next) => /// Resolves user claims to a object. /// Stores context information extracted from the current HTTP request. This information /// is primarily used to inject HTTP headers into downstream HTTP calls. + /// Contains the FoundationaLLM instance configuration settings. /// - public async Task InvokeAsync(HttpContext context, IUserClaimsProviderService claimsProviderService, ICallContext callContext) + public async Task InvokeAsync( + HttpContext context, + IUserClaimsProviderService claimsProviderService, + ICallContext callContext, + IOptions instanceSettings) { if (context.User is { Identity.IsAuthenticated: true }) { @@ -40,18 +47,29 @@ public async Task InvokeAsync(HttpContext context, IUserClaimsProviderService cl { // Extract from HTTP headers if available: var serializedIdentity = context.Request.Headers[Constants.HttpHeaders.UserIdentity].ToString(); - if (!string.IsNullOrEmpty(serializedIdentity)) + if (!string.IsNullOrWhiteSpace(serializedIdentity)) { callContext.CurrentUserIdentity = JsonConvert.DeserializeObject(serializedIdentity)!; } } var agentHint = context.Request.Headers[Constants.HttpHeaders.AgentHint].FirstOrDefault(); - if (!string.IsNullOrEmpty(agentHint)) + if (!string.IsNullOrWhiteSpace(agentHint)) { callContext.AgentHint = JsonConvert.DeserializeObject(agentHint); } + callContext.InstanceId = context.Request.RouteValues["instanceId"] as string; + if (!string.IsNullOrWhiteSpace(callContext.InstanceId) && callContext.InstanceId != instanceSettings.Value.Id) + { + // Throw 403 Forbidden since the instance ID within the route does not match the instance ID in the + // configuration settings: + context.Response.StatusCode = StatusCodes.Status403Forbidden; + await context.Response.WriteAsync("Access denied. Invalid instance ID."); + + return; // Short-circuit the request pipeline. + } + // Call the next delegate/middleware in the pipeline: await _next(context); } diff --git a/src/dotnet/Common/Models/Configuration/Instance/InstanceSettings.cs b/src/dotnet/Common/Models/Configuration/Instance/InstanceSettings.cs new file mode 100644 index 0000000000..3a302a56a6 --- /dev/null +++ b/src/dotnet/Common/Models/Configuration/Instance/InstanceSettings.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FoundationaLLM.Common.Models.Configuration.Instance +{ + /// + /// Provides configuration settings for the current FoundationaLLM deployment instance. + /// + public class InstanceSettings + { + /// + /// The unique identifier of the current FoundationaLLM deployment instance. + /// Format is a GUID. + /// + public required string Id { get; set; } + } +} diff --git a/src/dotnet/Common/Models/Context/CallContext.cs b/src/dotnet/Common/Models/Context/CallContext.cs index bd591ddb9d..bef8a9d720 100644 --- a/src/dotnet/Common/Models/Context/CallContext.cs +++ b/src/dotnet/Common/Models/Context/CallContext.cs @@ -16,5 +16,7 @@ public class CallContext : ICallContext public Agent? AgentHint { get; set; } /// public UnifiedUserIdentity? CurrentUserIdentity { get; set; } + /// + public string? InstanceId { get; set; } } } diff --git a/src/dotnet/Common/Models/ResourceProvider/ResourceBase.cs b/src/dotnet/Common/Models/ResourceProvider/ResourceBase.cs new file mode 100644 index 0000000000..77f862b9f6 --- /dev/null +++ b/src/dotnet/Common/Models/ResourceProvider/ResourceBase.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FoundationaLLM.Common.Models.ResourceProvider +{ + /// + /// Basic properties for all resources. + /// + public class ResourceBase + { + /// + /// The name of the resource. + /// + [JsonProperty("name")] + public required string Name { get; set; } + /// + /// The type of the resource. + /// + [JsonProperty("type")] + public required string Type { get; set; } + /// + /// The unique identifier of the resource. + /// + [JsonProperty("object_id")] + public required string ObjectId { get; set; } + /// + /// The description of the resource. + /// + [JsonProperty("description")] + public string? Description { get; set; } + } +} diff --git a/src/dotnet/Common/Services/BlobStorageService.cs b/src/dotnet/Common/Services/BlobStorageService.cs index ca820fa881..8d7d1274ea 100644 --- a/src/dotnet/Common/Services/BlobStorageService.cs +++ b/src/dotnet/Common/Services/BlobStorageService.cs @@ -60,6 +60,7 @@ public async Task WriteFileAsync( string containerName, string filePath, Stream fileContent, + string? contentType, CancellationToken cancellationToken = default) { var containerClient = _blobServiceClient.GetBlobContainerClient(containerName); @@ -67,7 +68,16 @@ public async Task WriteFileAsync( fileContent.Seek(0, SeekOrigin.Begin); - BlobUploadOptions options = new(); + BlobUploadOptions options = new() + { + HttpHeaders = new BlobHttpHeaders() + { + ContentType = string.IsNullOrWhiteSpace(contentType) + ? "application/json" + : contentType + } + }; + await blobClient.UploadAsync(fileContent, options, cancellationToken).ConfigureAwait(false); } @@ -76,11 +86,13 @@ public async Task WriteFileAsync( string containerName, string filePath, string fileContent, + string? contentType, CancellationToken cancellationToken = default) => await WriteFileAsync( containerName, filePath, new MemoryStream(Encoding.UTF8.GetBytes(fileContent)), + contentType, cancellationToken).ConfigureAwait(false); /// diff --git a/src/dotnet/Common/Services/DataLakeStorageService.cs b/src/dotnet/Common/Services/DataLakeStorageService.cs index a032c1a16f..923d3d2cf6 100644 --- a/src/dotnet/Common/Services/DataLakeStorageService.cs +++ b/src/dotnet/Common/Services/DataLakeStorageService.cs @@ -73,6 +73,7 @@ public Task WriteFileAsync( string containerName, string filePath, Stream fileContent, + string? contentType, CancellationToken cancellationToken) => throw new NotImplementedException(); @@ -81,6 +82,7 @@ public Task WriteFileAsync( string containerName, string filePath, string fileContent, + string? contentType, CancellationToken cancellationToken) => throw new NotImplementedException(); diff --git a/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs b/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs index d74d21cf10..cbf3724e89 100644 --- a/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs +++ b/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs @@ -1,5 +1,6 @@ using FoundationaLLM.Common.Exceptions; using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.ResourceProvider; using Microsoft.Extensions.Logging; @@ -22,6 +23,11 @@ public class ResourceProviderServiceBase : IResourceProviderService /// protected readonly ILogger _logger; + /// + /// The that provides instance-wide settings. + /// + protected readonly InstanceSettings _instanceSettings; + /// /// The name of the storage container name used by the resource provider to store its internal data. /// @@ -46,14 +52,17 @@ public class ResourceProviderServiceBase : IResourceProviderService /// /// Creates a new instance of the resource provider. /// + /// The that provides instance-wide settings. /// The providing storage services to the resource provider. /// The logger used for logging. public ResourceProviderServiceBase( + InstanceSettings instanceSettings, IStorageService storageService, ILogger logger) { _storageService = storageService; _logger = logger; + _instanceSettings = instanceSettings; // Kicks off the initialization on a separate thread and does not wait for it to complete. // The completion of the initialization process will be signaled by setting the _isInitialized property. @@ -74,6 +83,8 @@ public async Task Initialize() } } + #region IResourceProviderService + /// public async Task ExecuteAction(string actionPath) { @@ -101,6 +112,15 @@ public async Task> GetResourcesAsync(string resourcePath) where T : return await GetResourcesAsyncInternal(instances); } + /// + public async Task GetResourcesAsync(string resourcePath) + { + if (!_isInitialized) + throw new ResourceProviderException($"The resource provider {_name} is not initialized."); + var instances = GetResourceInstancesFromPath(resourcePath); + return await GetResourcesAsyncInternal(instances); + } + /// public T GetResource(string resourcePath) where T : class { @@ -128,6 +148,15 @@ public async Task UpsertResourceAsync(string resourcePath, T resource) where await UpsertResourceAsync(instances, resource); } + /// + public async Task UpsertResourceAsync(string resourcePath, string serializedResource) + { + if (!_isInitialized) + throw new ResourceProviderException($"The resource provider {_name} is not initialized."); + var instances = GetResourceInstancesFromPath(resourcePath); + await UpsertResourceAsync(instances, serializedResource); + } + /// public void UpsertResource(string resourcePath, T resource) where T : class { @@ -146,6 +175,15 @@ public async Task DeleteResourceAsync(string resourcePath) where T : class await DeleteResourceAsync(instances); } + /// + public async Task DeleteResourceAsync(string resourcePath) + { + if (!_isInitialized) + throw new ResourceProviderException($"The resource provider {_name} is not initialized."); + var instances = GetResourceInstancesFromPath(resourcePath); + await DeleteResourceAsync(instances); + } + /// public void DeleteResource(string resourcePath) where T : class { @@ -155,6 +193,8 @@ public void DeleteResource(string resourcePath) where T : class DeleteResource(instances); } + #endregion + /// /// The internal implementation of Initialize. Must be overridden in derived classes. /// @@ -195,6 +235,17 @@ protected virtual async Task> GetResourcesAsyncInternal(List + /// The internal implementation of GetResourcesAsync. Must be overridden in derived classes. + /// + /// The list of objects parsed from the resource path. + /// + protected virtual async Task GetResourcesAsyncInternal(List instances) + { + await Task.CompletedTask; + throw new NotImplementedException(); + } + /// /// The internal implementation of GetResource. Must be overridden in derived classes. /// @@ -235,6 +286,18 @@ protected virtual async Task UpsertResourceAsync(List i throw new NotImplementedException(); } + /// + /// The internal implementation of UpsertResourceAsync. Must be overridden in derived classes. + /// + /// The list of objects parsed from the resource path. + /// The serialized resource being created or updated. + /// + protected virtual async Task UpsertResourceAsync(List instances, string serializedResource) + { + await Task.CompletedTask; + throw new NotImplementedException(); + } + /// /// The internal implementation of DeleteResource. Must be overridden in derived classes. /// @@ -254,6 +317,35 @@ protected virtual async Task DeleteResourceAsync(List i throw new NotImplementedException(); } + /// + /// The internal implementation of DeleteResourceAsync. Must be overridden in derived classes. + /// + /// The list of objects parsed from the resource path. + /// + protected virtual async Task DeleteResourceAsync(List instances) + { + await Task.CompletedTask; + throw new NotImplementedException(); + } + + /// + /// Builds the resource unique identifier based on the resource path. + /// + /// The list of objects parsed from the resource path. + /// The unique resource identifier. + /// + protected string GetObjectId(List instances) + { + foreach (var instance in instances) + if (string.IsNullOrWhiteSpace(instance.ResourceType) + || string.IsNullOrWhiteSpace(instance.ResourceId) + || !(instance.Action == null)) + throw new ResourceProviderException("The provided resource path is not a valid resource identifier."); + + return $"/instances/{_instanceSettings.Id}/providers/{_name}/{string.Join("/", + instances.Select(i => $"{i.ResourceType}/{i.ResourceId}").ToArray())}"; + } + private List GetResourceInstancesFromPath(string resourcePath) { if (string.IsNullOrWhiteSpace(resourcePath)) diff --git a/src/dotnet/Common/Services/StorageServiceBase.cs b/src/dotnet/Common/Services/StorageServiceBase.cs index 132e948ebf..8aca963b0c 100644 --- a/src/dotnet/Common/Services/StorageServiceBase.cs +++ b/src/dotnet/Common/Services/StorageServiceBase.cs @@ -26,6 +26,11 @@ public abstract class StorageServiceBase /// protected readonly ILogger _logger; + /// + /// The optional instance name of the storage service. + /// + public string? InstanceName { get; set; } + /// /// Initializes a new instance of the with the specified options and logger. /// diff --git a/src/dotnet/GatekeeperAPI/Program.cs b/src/dotnet/GatekeeperAPI/Program.cs index 5e7d0e48e3..262e6aa358 100644 --- a/src/dotnet/GatekeeperAPI/Program.cs +++ b/src/dotnet/GatekeeperAPI/Program.cs @@ -6,6 +6,7 @@ using FoundationaLLM.Common.Extensions; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Middleware; +using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.Context; using FoundationaLLM.Common.OpenAPI; using FoundationaLLM.Common.Services; @@ -69,6 +70,8 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_APIs_GatekeeperAPI)); + builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_Instance)); // Register the downstream services and HTTP clients. RegisterDownstreamServices(builder); diff --git a/src/dotnet/Management/Management.csproj b/src/dotnet/Management/Management.csproj index 8b8945bda6..520d39134e 100644 --- a/src/dotnet/Management/Management.csproj +++ b/src/dotnet/Management/Management.csproj @@ -18,7 +18,9 @@ + + diff --git a/src/dotnet/Management/Models/ResourceBase.cs b/src/dotnet/Management/Models/ResourceBase.cs new file mode 100644 index 0000000000..1e49154c7b --- /dev/null +++ b/src/dotnet/Management/Models/ResourceBase.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FoundationaLLM.Management.Models +{ + public class ResourceBase + { + public required string Type { get; set; } + public required string Name { get; set; } + } +} diff --git a/src/dotnet/Management/Services/AgentManagementService.cs b/src/dotnet/Management/Services/AgentManagementService.cs index d41fb76958..c83a8a56e4 100644 --- a/src/dotnet/Management/Services/AgentManagementService.cs +++ b/src/dotnet/Management/Services/AgentManagementService.cs @@ -1,12 +1,67 @@ -using System; +using FoundationaLLM.Common.Constants; +using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Vectorization.Models.Resources; +using Microsoft.Extensions.DependencyInjection; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using FoundationaLLM.Agent.Models.Resources; +using FoundationaLLM.Agent.ResourceProviders; +using FoundationaLLM.Vectorization.ResourceProviders; namespace FoundationaLLM.Management.Services { - internal class AgentManagementService + public class AgentManagementService( + [FromKeyedServices(DependencyInjectionKeys.FoundationaLLM_Vectorization_ResourceProviderService)] + IResourceProviderService vectorizationResourceProviderService, + [FromKeyedServices(DependencyInjectionKeys.FoundationaLLM_Agent_ResourceProviderService)] + IResourceProviderService agentResourceProviderService) { + private readonly IResourceProviderService _vectorizationResourceProviderService = + vectorizationResourceProviderService; + private readonly IResourceProviderService _agentResourceProviderService = + agentResourceProviderService; + + private List? GetVectorContentSourceProfiles() + { + var contentSourceProfiles = _vectorizationResourceProviderService.GetResources( + $"/{VectorizationResourceTypeNames.ContentSourceProfiles}"); + + return contentSourceProfiles as List; + } + + private List? GetVectorIndexingProfiles() + { + var indexingProfiles = _vectorizationResourceProviderService.GetResources( + $"/{VectorizationResourceTypeNames.IndexingProfiles}"); + + return indexingProfiles as List; + } + + private List? GetContentSourceProfiles() + { + var contentSourceProfiles = _vectorizationResourceProviderService.GetResources( + $"/{VectorizationResourceTypeNames.ContentSourceProfiles}"); + + return contentSourceProfiles as List; + } + + private List? GetIndexingProfiles() + { + var indexingProfiles = _vectorizationResourceProviderService.GetResources( + $"/{VectorizationResourceTypeNames.IndexingProfiles}"); + + return indexingProfiles as List; + } + + private List? GetAgentReferences() + { + var agentReferences = _agentResourceProviderService.GetResources( + $"/{AgentResourceTypeNames.AgentReferences}"); + + return agentReferences as List; + } } } diff --git a/src/dotnet/ManagementAPI/Controllers/CachesController.cs b/src/dotnet/ManagementAPI/Controllers/CachesController.cs index 9183fea7ed..40108777a0 100644 --- a/src/dotnet/ManagementAPI/Controllers/CachesController.cs +++ b/src/dotnet/ManagementAPI/Controllers/CachesController.cs @@ -1,4 +1,5 @@ using Asp.Versioning; +using FoundationaLLM.Common.Constants; using FoundationaLLM.Common.Models.Cache; using FoundationaLLM.Common.Models.Configuration.Branding; using FoundationaLLM.Management.Interfaces; @@ -20,7 +21,7 @@ namespace FoundationaLLM.Management.API.Controllers [Authorize(Policy = "RequiredScope")] [ApiVersion(1.0)] [ApiController] - [Route("[controller]")] + [Route($"instances/{{instanceId}}/providersX/{ResourceProviderNames.FoundationaLLM_Configuration}/caches")] public class CachesController( ICacheManagementService cacheManagementService) : ControllerBase { diff --git a/src/dotnet/ManagementAPI/Controllers/ConfigurationsController.cs b/src/dotnet/ManagementAPI/Controllers/ConfigurationsController.cs index 1430361bbd..66ade3d50f 100644 --- a/src/dotnet/ManagementAPI/Controllers/ConfigurationsController.cs +++ b/src/dotnet/ManagementAPI/Controllers/ConfigurationsController.cs @@ -1,4 +1,5 @@ using Asp.Versioning; +using FoundationaLLM.Common.Constants; using FoundationaLLM.Common.Models.Cache; using FoundationaLLM.Common.Models.Configuration.Branding; using FoundationaLLM.Management.Interfaces; @@ -20,7 +21,7 @@ namespace FoundationaLLM.Management.API.Controllers [Authorize(Policy = "RequiredScope")] [ApiVersion(1.0)] [ApiController] - [Route("[controller]")] + [Route($"instances/{{instanceId}}/providersX/{ResourceProviderNames.FoundationaLLM_Configuration}/configurations")] public class ConfigurationsController( IConfigurationManagementService configurationManagementService) : ControllerBase { diff --git a/src/dotnet/ManagementAPI/Controllers/ResourceController.cs b/src/dotnet/ManagementAPI/Controllers/ResourceController.cs new file mode 100644 index 0000000000..ea89b37cef --- /dev/null +++ b/src/dotnet/ManagementAPI/Controllers/ResourceController.cs @@ -0,0 +1,104 @@ +using Asp.Versioning; +using FoundationaLLM.Common.Exceptions; +using FoundationaLLM.Common.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace FoundationaLLM.Management.API.Controllers +{ + /// + /// Provides methods to manage resources. + /// + /// The list of resource providers. + /// The used for logging. + [Authorize] + [Authorize(Policy = "RequiredScope")] + [ApiVersion(1.0)] + [ApiController] + [Route($"instances/{{instanceId}}/providers/{{resourceProvider}}")] + public class ResourceController( + IEnumerable resourceProviderServices, + ILogger logger) : Controller + { + private readonly Dictionary _resourceProviderServices = + resourceProviderServices.ToDictionary( + rps => rps.Name); + private readonly ILogger _logger = logger; + + /// + /// Gets one or more resources. + /// + /// The FoundationaLLM instance identifier. + /// The name of the resource provider that should handle the request. + /// The logical path of the resource type. + /// + [HttpGet("{*resourcePath}", Name = "GetResources")] + public async Task GetResources(string instanceId, string resourceProvider, string resourcePath) => + await HandleRequest( + resourceProvider, + resourcePath, + async (resourceProviderService) => + { + var result = await resourceProviderService.GetResourcesAsync(resourcePath); + return new OkObjectResult(result); + }); + + /// + /// Creates or updates resources. + /// + /// The FoundationaLLM instance identifier. + /// The name of the resource provider that should handle the request. + /// The logical path of the resource type. + /// The serialized resource to be created or updated. + /// + [HttpPost("{*resourcePath}", Name = "UpsertResource")] + public async Task UpsertResource(string instanceId, string resourceProvider, string resourcePath, [FromBody] object serializedResource) => + await HandleRequest( + resourceProvider, + resourcePath, + async (resourceProviderService) => + { + await resourceProviderService.UpsertResourceAsync(resourcePath, serializedResource.ToString()!); + return new OkResult(); + }); + + /// + /// Deletes a resource. + /// + /// The FoundationaLLM instance identifier. + /// The name of the resource provider that should handle the request. + /// The logical path of the resource type. + /// + [HttpDelete("{*resourcePath}", Name = "DeleteResource")] + public async Task DeleteResource(string instanceId, string resourceProvider, string resourcePath) => + await HandleRequest( + resourceProvider, + resourcePath, + async (resourceProviderService) => + { + await resourceProviderService.DeleteResourceAsync(resourcePath); + return new OkResult(); + }); + + private async Task HandleRequest(string resourceProvider, string resourcePath, Func> handler) + { + if (!_resourceProviderServices.TryGetValue(resourceProvider, out var resourceProviderService)) + return new NotFoundResult(); + + try + { + return await handler(resourceProviderService); + } + catch (ResourceProviderException ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "The {ResourceProviderName} encountered an error while handling the request for {ResourcePath}.", resourceProvider, resourcePath); + return StatusCode(StatusCodes.Status500InternalServerError, $"The {resourceProvider} encountered an error while handling the request for {resourcePath}."); + } + } + } +} diff --git a/src/dotnet/ManagementAPI/Controllers/StatusController.cs b/src/dotnet/ManagementAPI/Controllers/StatusController.cs index 8d7a9bd7ad..d9175e4144 100644 --- a/src/dotnet/ManagementAPI/Controllers/StatusController.cs +++ b/src/dotnet/ManagementAPI/Controllers/StatusController.cs @@ -11,7 +11,7 @@ namespace FoundationaLLM.Management.API.Controllers [Authorize(Policy = "RequiredScope")] [ApiVersion(1.0)] [ApiController] - [Route("[controller]")] + [Route("status")] public class StatusController : ControllerBase { /// diff --git a/src/dotnet/ManagementAPI/Program.cs b/src/dotnet/ManagementAPI/Program.cs index eeea010c78..cdf3c19ad3 100644 --- a/src/dotnet/ManagementAPI/Program.cs +++ b/src/dotnet/ManagementAPI/Program.cs @@ -5,11 +5,13 @@ using Microsoft.Extensions.Options; using Swashbuckle.AspNetCore.SwaggerGen; using System.Net.Http; +using FoundationaLLM.Agent.ResourceProviders; using FoundationaLLM.Common.Authentication; using FoundationaLLM.Common.Constants; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Middleware; using FoundationaLLM.Common.Models.Configuration.Branding; +using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.Context; using FoundationaLLM.Common.OpenAPI; using FoundationaLLM.Common.Services; @@ -19,8 +21,10 @@ using FoundationaLLM.Management.Models.Configuration; using FoundationaLLM.Management.Services; using FoundationaLLM.Management.Services.APIServices; +using FoundationaLLM.Vectorization.ResourceProviders; using Microsoft.Identity.Web; using Polly; +using Microsoft.Extensions.DependencyInjection; namespace FoundationaLLM.Management.API { @@ -43,10 +47,13 @@ public static void Main(string[] args) { options.Connect(builder.Configuration[AppConfigurationKeys.FoundationaLLM_AppConfig_ConnectionString]); options.ConfigureKeyVault(options => { options.SetCredential(new DefaultAzureCredential()); }); + options.Select(AppConfigurationKeyFilters.FoundationaLLM_Instance); options.Select(AppConfigurationKeyFilters.FoundationaLLM_APIs); options.Select(AppConfigurationKeyFilters.FoundationaLLM_CosmosDB); options.Select(AppConfigurationKeyFilters.FoundationaLLM_Branding); options.Select(AppConfigurationKeyFilters.FoundationaLLM_ManagementAPI_Entra); + options.Select(AppConfigurationKeyFilters.FoundationaLLM_Vectorization); + options.Select(AppConfigurationKeyFilters.FoundationaLLM_Agent); }); if (builder.Environment.IsDevelopment()) builder.Configuration.AddJsonFile("appsettings.development.json", true, true); @@ -71,6 +78,8 @@ public static void Main(string[] args) builder.Services.AddOptions() .Configure(o => o.ConnectionString = builder.Configuration[AppConfigurationKeys.FoundationaLLM_AppConfig_ConnectionString]!); + builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_Instance)); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -83,7 +92,73 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddScoped(); - // Register the authentication services + //---------------------------- + // Resource providers + //---------------------------- + + #region Vectorization resource provider + + builder.Services.AddOptions( + DependencyInjectionKeys.FoundationaLLM_Vectorization_ResourceProviderService) + .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_Vectorization_ResourceProviderService_Storage)); + + builder.Services.AddSingleton( sp => + { + var settings = sp.GetRequiredService>() + .Get(DependencyInjectionKeys.FoundationaLLM_Vectorization_ResourceProviderService); + var logger = sp.GetRequiredService>(); + + return new BlobStorageService( + Options.Create(settings), + logger) + { + InstanceName = DependencyInjectionKeys.FoundationaLLM_Vectorization_ResourceProviderService + }; + }); + + // Register the resource provider services (cannot use Keyed singletons due to the Microsoft Identity package being incompatible): + builder.Services.AddSingleton(sp => + new VectorizationResourceProviderService( + sp.GetRequiredService>(), + sp.GetRequiredService>() + .Single(s => s.InstanceName == DependencyInjectionKeys.FoundationaLLM_Vectorization_ResourceProviderService), + sp.GetRequiredService>())); + + #endregion + + #region Agent resource provider + + builder.Services.AddOptions( + DependencyInjectionKeys.FoundationaLLM_Agent_ResourceProviderService) + .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_Agent_ResourceProviderService_Storage)); + + builder.Services.AddSingleton(sp => + { + var settings = sp.GetRequiredService>() + .Get(DependencyInjectionKeys.FoundationaLLM_Agent_ResourceProviderService); + var logger = sp.GetRequiredService>(); + + return new BlobStorageService( + Options.Create(settings), + logger) + { + InstanceName = DependencyInjectionKeys.FoundationaLLM_Agent_ResourceProviderService + }; + }); + + builder.Services.AddSingleton(sp => + new AgentResourceProviderService( + sp.GetRequiredService>(), + sp.GetRequiredService>() + .Single(s => s.InstanceName == DependencyInjectionKeys.FoundationaLLM_Agent_ResourceProviderService), + sp.GetRequiredService>())); + + #endregion + + // Activate all resource providers (give them a chance to initialize). + builder.Services.ActivateSingleton>(); + + // Register the authentication services: RegisterAuthConfiguration(builder); builder.Services.AddApplicationInsightsTelemetry(new ApplicationInsightsServiceOptions @@ -137,11 +212,6 @@ public static void Main(string[] args) }) .AddSwaggerGenNewtonsoftSupport(); - builder.Services.Configure(options => - { - options.LowercaseUrls = true; - }); - var app = builder.Build(); // Set the CORS policy before other middleware. diff --git a/src/dotnet/SemanticKernelAPI/Program.cs b/src/dotnet/SemanticKernelAPI/Program.cs index 85d789dcf2..76d9c99bb2 100644 --- a/src/dotnet/SemanticKernelAPI/Program.cs +++ b/src/dotnet/SemanticKernelAPI/Program.cs @@ -3,6 +3,7 @@ using FoundationaLLM.Common.Constants; using FoundationaLLM.Common.Extensions; using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.OpenAPI; using FoundationaLLM.SemanticKernel.Core.Interfaces; using FoundationaLLM.SemanticKernel.Core.Models.ConfigurationOptions; @@ -54,6 +55,8 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_APIs_SemanticKernelAPI)); + builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_Instance)); builder.Services.AddTransient(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle diff --git a/src/dotnet/Vectorization/Models/Resources/ContentSourceProfile.cs b/src/dotnet/Vectorization/Models/Resources/ContentSourceProfile.cs index 149e92e975..f2ba5438cf 100644 --- a/src/dotnet/Vectorization/Models/Resources/ContentSourceProfile.cs +++ b/src/dotnet/Vectorization/Models/Resources/ContentSourceProfile.cs @@ -4,7 +4,7 @@ namespace FoundationaLLM.Vectorization.Models.Resources { /// - /// Provides detials about a content source. + /// Provides details about a content source. /// public class ContentSourceProfile : VectorizationProfileBase { diff --git a/src/dotnet/Vectorization/ResourceProviders/VectorizationResourceProviderService.cs b/src/dotnet/Vectorization/ResourceProviders/VectorizationResourceProviderService.cs index 86e19b89e9..12a128ef80 100644 --- a/src/dotnet/Vectorization/ResourceProviders/VectorizationResourceProviderService.cs +++ b/src/dotnet/Vectorization/ResourceProviders/VectorizationResourceProviderService.cs @@ -1,10 +1,12 @@ using FoundationaLLM.Common.Constants; using FoundationaLLM.Common.Exceptions; using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Services.ResourceProviders; using FoundationaLLM.Vectorization.Models.Resources; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Newtonsoft.Json; using System.Linq; using System.Text; @@ -15,9 +17,11 @@ namespace FoundationaLLM.Vectorization.ResourceProviders /// Implements the FoundationaLLM.Vectorization resource provider. /// public class VectorizationResourceProviderService( + IOptions instanceOptions, [FromKeyedServices(DependencyInjectionKeys.FoundationaLLM_Vectorization_ResourceProviderService)] IStorageService storageService, ILogger logger) : ResourceProviderServiceBase( + instanceOptions.Value, storageService, logger) { @@ -108,14 +112,57 @@ protected override async Task InitializeInternal() protected override T GetResourceInternal(List instances) where T: class => instances[0].ResourceType switch { - VectorizationResourceTypeNames.ContentSourceProfiles => GetContentSourceProfiles(instances), + VectorizationResourceTypeNames.ContentSourceProfiles => GetContentSourceProfile(instances), VectorizationResourceTypeNames.TextPartitioningProfiles => GetTextPartitioningProfile(instances), VectorizationResourceTypeNames.TextEmbeddingProfiles => GetTextEmbeddingProfile(instances), VectorizationResourceTypeNames.IndexingProfiles => GetIndexingProfile(instances), _ => throw new ResourceProviderException($"The resource type {instances[0].ResourceType} is not supported by the {_name} resource manager.") }; - private T GetContentSourceProfiles(List instances) where T: class + /// + protected override List GetResourcesInternal(List instances) where T : class => + instances[0].ResourceType switch + { + VectorizationResourceTypeNames.ContentSourceProfiles => GetContentSourceProfiles(instances), + VectorizationResourceTypeNames.TextPartitioningProfiles => GetTextPartitioningProfiles(instances), + VectorizationResourceTypeNames.TextEmbeddingProfiles => GetTextEmbeddingProfiles(instances), + VectorizationResourceTypeNames.IndexingProfiles => GetIndexingProfiles(instances), + _ => throw new ResourceProviderException($"The resource type {instances[0].ResourceType} is not supported by the {_name} resource manager.") + }; + + private List GetContentSourceProfiles(List instances) where T : class + { + if (typeof(T) != typeof(ContentSourceProfile)) + throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({instances[0].ResourceType})."); + + return _contentSourceProfiles.Values.Cast().ToList(); + } + + private List GetTextPartitioningProfiles(List instances) where T : class + { + if (typeof(T) != typeof(TextPartitioningProfile)) + throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({instances[0].ResourceType})."); + + return _textPartitioningProfiles.Values.Cast().ToList(); + } + + private List GetTextEmbeddingProfiles(List instances) where T : class + { + if (typeof(T) != typeof(TextEmbeddingProfile)) + throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({instances[0].ResourceType})."); + + return _textEmbeddingProfiles.Values.Cast().ToList(); + } + + private List GetIndexingProfiles(List instances) where T : class + { + if (typeof(T) != typeof(IndexingProfile)) + throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({instances[0].ResourceType})."); + + return _indexingProfiles.Values.Cast().ToList(); + } + + private T GetContentSourceProfile(List instances) where T: class { if (instances.Count != 1) throw new ResourceProviderException($"Invalid resource path"); diff --git a/src/dotnet/Vectorization/Services/VectorizationStates/BlobStorageVectorizationStateService.cs b/src/dotnet/Vectorization/Services/VectorizationStates/BlobStorageVectorizationStateService.cs index ef1d34e316..7efb854418 100644 --- a/src/dotnet/Vectorization/Services/VectorizationStates/BlobStorageVectorizationStateService.cs +++ b/src/dotnet/Vectorization/Services/VectorizationStates/BlobStorageVectorizationStateService.cs @@ -77,6 +77,7 @@ await _storageService.WriteFileAsync( BLOB_STORAGE_CONTAINER_NAME, artifactPath, artifact.Content!, + default, default); artifact.CanonicalId = artifactPath; } @@ -86,6 +87,7 @@ await _storageService.WriteFileAsync( BLOB_STORAGE_CONTAINER_NAME, $"{persistenceIdentifier}.json", content, + default, default); } } diff --git a/src/dotnet/VectorizationAPI/Program.cs b/src/dotnet/VectorizationAPI/Program.cs index 1463c377c6..b3507abc49 100644 --- a/src/dotnet/VectorizationAPI/Program.cs +++ b/src/dotnet/VectorizationAPI/Program.cs @@ -20,6 +20,7 @@ using Microsoft.ApplicationInsights.AspNetCore.Extensions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; +using FoundationaLLM.Common.Models.Configuration.Instance; var builder = WebApplication.CreateBuilder(args); @@ -58,6 +59,8 @@ }); // Add configurations to the container +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_Instance)); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(AppConfigurationKeys.FoundationaLLM_Vectorization_VectorizationWorker)); diff --git a/src/dotnet/VectorizationWorker/Program.cs b/src/dotnet/VectorizationWorker/Program.cs index ad61f014c6..41734c6402 100644 --- a/src/dotnet/VectorizationWorker/Program.cs +++ b/src/dotnet/VectorizationWorker/Program.cs @@ -3,6 +3,7 @@ using FoundationaLLM.Common.Authentication; using FoundationaLLM.Common.Constants; using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.OpenAPI; using FoundationaLLM.Common.Services; using FoundationaLLM.Common.Services.Tokenizers; @@ -56,6 +57,9 @@ }); // Add configurations to the container +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_Instance)); + builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(AppConfigurationKeys.FoundationaLLM_Vectorization_VectorizationWorker)); diff --git a/tests/dotnet/Common.Tests/Middleware/AgentHintMiddlewareTests.cs b/tests/dotnet/Common.Tests/Middleware/AgentHintMiddlewareTests.cs index fcb66248f6..239baae1f3 100644 --- a/tests/dotnet/Common.Tests/Middleware/AgentHintMiddlewareTests.cs +++ b/tests/dotnet/Common.Tests/Middleware/AgentHintMiddlewareTests.cs @@ -11,6 +11,8 @@ using System.Text; using System.Threading.Tasks; using FoundationaLLM.Common.Models.Metadata; +using FoundationaLLM.Common.Models.Configuration.Instance; +using Microsoft.Extensions.Options; namespace FoundationaLLM.Common.Tests.Middleware { @@ -23,6 +25,7 @@ public async Task InvokeAsync_WithAuthenticatedUser_ShouldSetCurrentUserIdentity var context = new DefaultHttpContext(); var claimsProviderService = Substitute.For(); var callContext = Substitute.For(); + var instanceSettings = Options.Create(Substitute.For()); var middleware = new CallContextMiddleware(next: _ => Task.FromResult(0)); context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { @@ -33,7 +36,7 @@ public async Task InvokeAsync_WithAuthenticatedUser_ShouldSetCurrentUserIdentity }, "mock")); // Act - await middleware.InvokeAsync(context, claimsProviderService, callContext); + await middleware.InvokeAsync(context, claimsProviderService, callContext, instanceSettings); // Assert claimsProviderService.Received(1).GetUserIdentity(context.User); @@ -47,12 +50,13 @@ public async Task InvokeAsync_WithUnauthenticatedUser_ShouldSetCurrentUserIdenti var context = new DefaultHttpContext(); var claimsProviderService = Substitute.For(); var callContext = Substitute.For(); + var instanceSettings = Options.Create(Substitute.For()); var middleware = new CallContextMiddleware(next: _ => Task.FromResult(0)); var userIdentity = new UnifiedUserIdentity { Username = "testuser@example.com", UPN = "testuser@example.com", Name = "testuser" }; context.Request.Headers[Constants.HttpHeaders.UserIdentity] = JsonConvert.SerializeObject(userIdentity); // Act - await middleware.InvokeAsync(context, claimsProviderService, callContext); + await middleware.InvokeAsync(context, claimsProviderService, callContext, instanceSettings); // Assert callContext.Received(1).CurrentUserIdentity = Arg.Is(x => x.Username == userIdentity.Username && x.UPN == userIdentity.UPN && x.Name == userIdentity.Name); @@ -65,6 +69,7 @@ public async Task InvokeAsync_WithAgentHint_ShouldSetAgentHint() var context = new DefaultHttpContext(); var claimsProviderService = Substitute.For(); var callContext = Substitute.For(); + var instanceSettings = Options.Create(Substitute.For()); var middleware = new CallContextMiddleware(next: _ => Task.FromResult(0)); var agentHint = new Agent { @@ -74,7 +79,7 @@ public async Task InvokeAsync_WithAgentHint_ShouldSetAgentHint() context.Request.Headers[Constants.HttpHeaders.AgentHint] = JsonConvert.SerializeObject(agentHint); // Act - await middleware.InvokeAsync(context, claimsProviderService, callContext); + await middleware.InvokeAsync(context, claimsProviderService, callContext, instanceSettings); // Assert callContext.Received(1).AgentHint = Arg.Is(x => x.Name == agentHint.Name && x.Private == agentHint.Private);