From e260e6b477056fb1806e8b0da8aed3382ec513cc Mon Sep 17 00:00:00 2001 From: Ciprian Jichici Date: Sun, 22 Sep 2024 22:56:17 +0300 Subject: [PATCH] Merge pull request #1684 from solliancenet/cj-conversation-resource-provider FoundationaLLM.Conversation resource provider --- .../FoundationaLLM.template.json | 1 + .../FoundationaLLM.template.json | 1 + docs/release-notes/breaking-changes.md | 44 +- src/FoundationaLLM.sln | 9 +- src/dotnet/AIModel/Models/AIModelReference.cs | 2 +- .../AIModelResourceProviderService.cs | 304 ++----- .../Agent/Models/Resources/AgentReference.cs | 2 +- .../Models/Resources/AgentReferenceStore.cs | 31 - .../AgentResourceProviderService.cs | 439 +++------- .../AttachmentResourceProviderService.cs | 192 ++--- .../Interfaces/IAuthorizationCore.cs | 29 +- .../AuthorizationResourceProviderService.cs | 61 +- .../Services/AuthorizationCore.cs | 322 ++++--- .../Services/AuthorizationService.cs | 79 +- .../Services/DependencyInjection.cs | 6 +- .../Services/NullAuthorizationService.cs | 34 +- .../ActionAuthorizationRequestValidator.cs | 23 +- .../Controllers/RoleAssignmentsController.cs | 12 +- .../AzureOpenAIResourceProviderService.cs | 147 ++-- .../ResourceProviders/DependencyInjection.cs | 25 +- src/dotnet/Common/Common.csproj | 2 + .../Authorization/AuthorizableOperations.cs | 23 + .../Constants/Chat/ConversationTypes.cs} | 4 +- .../Constants/EventSetEventNamespaces.cs | 5 + .../AIModelResourceProviderMetadata.cs | 16 +- .../AgentResourceProviderMetadata.cs | 16 +- .../AttachmentResourceProviderMetadata.cs | 13 +- .../AuthorizationResourceProviderMetadata.cs | 17 +- .../AzureOpenAIResourceProviderMetadata.cs | 33 +- .../ConfigurationResourceProviderMetadata.cs | 23 +- .../ConversationResourceProviderMetadata.cs | 32 + .../ConversationResourceTypeNames.cs | 13 + .../DataSourceResourceProviderMetadata.cs | 18 +- .../PromptResourceProviderMetadata.cs | 18 +- .../ResourceProviderNames.cs | 8 +- .../VectorizationResourceProviderMetadata.cs | 70 +- .../AuthorizationServiceExtensions.cs | 55 -- .../ResourceProviderServiceExtensions.cs | 239 ----- .../Interfaces/IAuthorizationService.cs | 48 +- .../Interfaces/ICosmosDBService.cs} | 20 +- .../Interfaces/IManagementProviderService.cs | 4 +- .../Interfaces/IResourceProviderService.cs | 53 +- .../ActionAuthorizationRequest.cs | 23 +- .../ActionAuthorizationResult.cs | 6 +- .../ResourcePathAuthorizationResult.cs | 54 ++ ...lt.cs => RoleAssignmentOperationResult.cs} | 6 +- .../Authorization/UserAuthorizationContext.cs | 28 + .../Common/Models/Chat/AttachmentDetail.cs | 28 - .../Models/Conversation/AttachmentDetail.cs | 42 + .../ChatSessionProperties.cs | 2 +- .../{Chat => Conversation}/Completion.cs | 2 +- .../CompletionPrompt.cs | 2 +- .../{Chat => Conversation}/DocumentVector.cs | 2 +- .../Models/{Chat => Conversation}/Message.cs | 2 +- .../{Chat => Conversation}/MessageContent.cs | 2 +- .../MessageHistoryItem.cs | 2 +- .../Request/CompletionRequestBase.cs | 2 +- .../Conversation/Conversation.cs} | 16 +- .../Models/ResourceProviders/ResourceBase.cs | 8 +- .../ResourceProviders/ResourceFilter.cs | 15 +- .../Models/ResourceProviders/ResourceName.cs | 2 +- .../ResourceNameCheckResult.cs | 14 + .../Models/ResourceProviders/ResourcePath.cs | 147 +++- .../ResourceProviderGetResult.cs | 6 - .../ResourceProviderLoadOptions.cs | 24 + .../ResourceProviderOptions.cs | 13 - .../ResourceProviderUpsertResult.cs | 4 +- .../ResourceReferenceList`1.cs | 5 + .../ResourceTypeDescriptor.cs | 15 +- .../ResourceProviders/ResourceTypeInstance.cs | 8 +- .../Services/API/HttpClientFactoryService.cs | 2 +- .../Services/Azure/AzureCosmosDBService.cs} | 36 +- .../Common/Services/DependencyInjection.cs | 66 +- .../Events/AzureEventGridEventService.cs | 4 + ...esourceProviderResourceReferenceStore`1.cs | 137 ++- .../ResourceProviderServiceBase.cs | 815 ++++++++++++++---- .../Services/Security/DependencyInjection.cs | 7 +- .../Services/Storage/NullStorageService.cs | 37 + src/dotnet/Common/Utils/ResourcePathUtils.cs | 1 + .../ConfigurationResourceProviderService.cs | 391 +++------ src/dotnet/Conversation/Conversation.csproj | 15 + .../ConversationResourceProviderService.cs | 75 ++ .../ResourceProviders/DependencyInjection.cs | 50 ++ src/dotnet/Core/Interfaces/ICoreService.cs | 8 +- src/dotnet/Core/Services/CoreService.cs | 73 +- .../Services/CosmosDbChangeFeedService.cs | 29 +- .../Core/Services/UserProfileService.cs | 4 +- .../Controllers/CompletionsController.cs | 9 +- .../CoreAPI/Controllers/SessionsController.cs | 9 +- src/dotnet/CoreAPI/CoreAPI.csproj | 1 + src/dotnet/CoreAPI/Program.cs | 18 +- .../RESTClients/CompletionRestClient.cs | 8 +- .../Clients/RESTClients/SessionRESTClient.cs | 8 +- src/dotnet/CoreClient/CoreClient.cs | 2 +- .../Interfaces/ICompletionRESTClient.cs | 2 +- .../CoreClient/Interfaces/ICoreClient.cs | 2 +- .../Interfaces/ISessionRESTClient.cs | 4 +- src/dotnet/CoreWorker/Program.cs | 4 +- .../DataSource/Models/DataSourceReference.cs | 2 +- .../Models/DataSourceReferenceStore.cs | 35 - .../DataSourceResourceProviderService.cs | 514 +++-------- src/dotnet/Gateway/Services/GatewayCore.cs | 2 +- .../Controllers/ResourceController.cs | 9 +- .../KnowledgeManagementOrchestration.cs | 25 +- .../Orchestration/OrchestrationBuilder.cs | 42 +- .../LLMOrchestrationServiceManager.cs | 15 +- .../Services/OrchestrationService.cs | 6 +- .../Models/{Resources => }/PromptReference.cs | 4 +- .../Models/Resources/PromptReferenceStore.cs | 31 - .../PromptResourceProviderService.cs | 337 ++------ .../KnowledgeManagementContextPlugin.cs | 2 +- .../Client/VectorizationServiceClient.cs | 4 +- .../VectorizationRequestExtensions.cs | 15 +- ...zationResourceProviderServiceExtensions.cs | 4 +- .../VectorizationStateServiceExtensions.cs | 2 +- .../IVectorizationRequestProcessor.cs | 3 +- .../Interfaces/IVectorizationService.cs | 3 +- .../Interfaces/IVectorizationServiceClient.cs | 3 +- .../VectorizationRequestProcessingContext.cs | 8 + .../VectorizationResourceProviderService.cs | 137 +-- .../ContentSourceServiceFactory.cs | 2 +- .../Pipelines/PipelineExecutionService.cs | 22 +- .../Services/RequestManagerService.cs | 43 +- .../LocalVectorizationRequestProcessor.cs | 4 +- .../RemoteVectorizationRequestProcessor.cs | 4 +- .../Services/Text/IndexingServiceFactory.cs | 7 +- .../Text/TextEmbeddingServiceFactory.cs | 4 +- .../Text/TextSplitterServiceFactory.cs | 4 +- .../AsynchronousVectorizationService.cs | 2 +- .../SynchronousVectorizationService.cs | 8 +- .../VectorizationRequestController.cs | 14 +- src/ui/UserPortal/plugins/fileIconPlugin.ts | 4 +- .../Models/Chat/CompletionPromptTests.cs | 2 +- .../Models/Chat/CompletionTests.cs | 2 +- .../Models/Chat/DocumentVectorTests.cs | 2 +- .../Models/Chat/MessageHistoryItemTests.cs | 2 +- .../Common.Tests/Models/Chat/MessageTests.cs | 2 +- .../Common.Tests/Models/Chat/SessionTests.cs | 10 +- .../Orchestration/CompletionRequestTests.cs | 2 +- .../ResourceNameCheckResultTests.cs | 4 +- .../ResourceProvider/ResourcePathTests.cs | 11 +- .../ResourceProviderUpsertResultTests.cs | 3 +- .../ResourceTypeDescriptorTests.cs | 9 +- .../ResourceTypeInstanceTests.cs | 4 +- .../Core.Client.Tests/CoreClientTests.cs | 7 +- ...0001_ResourceProviderResourceReferences.cs | 3 +- .../IAgentConversationTestService.cs | 2 +- .../Interfaces/ICoreAPITestManager.cs | 2 +- .../Services/AgentConversationTestService.cs | 2 +- .../Services/CoreAPITestManager.cs | 4 +- .../Setup/TestServicesInitializer.cs | 5 +- .../Core.Tests/Services/CoreServiceTests.cs | 2 +- .../Services/OrchestrationAPIServiceTests.cs | 2 +- .../Resources/AIModelManagementClientTests.cs | 5 +- .../Resources/AgentManagementClientTests.cs | 9 +- .../AttachmentManagementClientTests.cs | 5 +- .../ConfigurationManagementClientTests.cs | 10 +- .../DataSourceManagementClientTests.cs | 12 +- .../Resources/PromptManagementClientTests.cs | 10 +- .../VectorizationManagementClientTests.cs | 30 +- .../ManagementClientTests.cs | 2 - 161 files changed, 3172 insertions(+), 3104 deletions(-) delete mode 100644 src/dotnet/Agent/Models/Resources/AgentReferenceStore.cs create mode 100644 src/dotnet/Common/Constants/Authorization/AuthorizableOperations.cs rename src/dotnet/{Core/Models/SessionTypes.cs => Common/Constants/Chat/ConversationTypes.cs} (82%) create mode 100644 src/dotnet/Common/Constants/ResourceProviders/ConversationResourceProviderMetadata.cs create mode 100644 src/dotnet/Common/Constants/ResourceProviders/ConversationResourceTypeNames.cs delete mode 100644 src/dotnet/Common/Extensions/AuthorizationServiceExtensions.cs delete mode 100644 src/dotnet/Common/Extensions/ResourceProviderServiceExtensions.cs rename src/dotnet/{Core/Interfaces/ICosmosDbService.cs => Common/Interfaces/ICosmosDBService.cs} (88%) create mode 100644 src/dotnet/Common/Models/Authorization/ResourcePathAuthorizationResult.cs rename src/dotnet/Common/Models/Authorization/{RoleAssignmentResult.cs => RoleAssignmentOperationResult.cs} (59%) create mode 100644 src/dotnet/Common/Models/Authorization/UserAuthorizationContext.cs delete mode 100644 src/dotnet/Common/Models/Chat/AttachmentDetail.cs create mode 100644 src/dotnet/Common/Models/Conversation/AttachmentDetail.cs rename src/dotnet/Common/Models/{Chat => Conversation}/ChatSessionProperties.cs (86%) rename src/dotnet/Common/Models/{Chat => Conversation}/Completion.cs (80%) rename src/dotnet/Common/Models/{Chat => Conversation}/CompletionPrompt.cs (96%) rename src/dotnet/Common/Models/{Chat => Conversation}/DocumentVector.cs (95%) rename src/dotnet/Common/Models/{Chat => Conversation}/Message.cs (98%) rename src/dotnet/Common/Models/{Chat => Conversation}/MessageContent.cs (93%) rename src/dotnet/Common/Models/{Chat => Conversation}/MessageHistoryItem.cs (94%) rename src/dotnet/Common/Models/{Chat/Session.cs => ResourceProviders/Conversation/Conversation.cs} (82%) create mode 100644 src/dotnet/Common/Models/ResourceProviders/ResourceProviderLoadOptions.cs delete mode 100644 src/dotnet/Common/Models/ResourceProviders/ResourceProviderOptions.cs rename src/dotnet/{Core/Services/CosmosDbService.cs => Common/Services/Azure/AzureCosmosDBService.cs} (91%) create mode 100644 src/dotnet/Common/Services/Storage/NullStorageService.cs create mode 100644 src/dotnet/Conversation/Conversation.csproj create mode 100644 src/dotnet/Conversation/ResourceProviders/ConversationResourceProviderService.cs create mode 100644 src/dotnet/Conversation/ResourceProviders/DependencyInjection.cs delete mode 100644 src/dotnet/DataSource/Models/DataSourceReferenceStore.cs rename src/dotnet/Prompt/Models/{Resources => }/PromptReference.cs (90%) delete mode 100644 src/dotnet/Prompt/Models/Resources/PromptReferenceStore.cs diff --git a/deploy/quick-start/data/resource-provider/FoundationaLLM.Prompt/FoundationaLLM.template.json b/deploy/quick-start/data/resource-provider/FoundationaLLM.Prompt/FoundationaLLM.template.json index 7b200c040e..dbb626d5ae 100644 --- a/deploy/quick-start/data/resource-provider/FoundationaLLM.Prompt/FoundationaLLM.template.json +++ b/deploy/quick-start/data/resource-provider/FoundationaLLM.Prompt/FoundationaLLM.template.json @@ -1,4 +1,5 @@ { + "type": "multipart", "name": "FoundationaLLM", "object_id": "/instances/${env:FOUNDATIONALLM_INSTANCE_ID}/providers/FoundationaLLM.Prompt/prompts/FoundationaLLM", "description": "Describes the persona and context for the FoundationaLLM agent, useful for answering questions about FoundationaLLM (FLLM).", diff --git a/deploy/standard/data/resource-provider/FoundationaLLM.Prompt/FoundationaLLM.template.json b/deploy/standard/data/resource-provider/FoundationaLLM.Prompt/FoundationaLLM.template.json index b57377df37..c94a39e103 100644 --- a/deploy/standard/data/resource-provider/FoundationaLLM.Prompt/FoundationaLLM.template.json +++ b/deploy/standard/data/resource-provider/FoundationaLLM.Prompt/FoundationaLLM.template.json @@ -1,4 +1,5 @@ { + "type": "multipart", "name": "FoundationaLLM", "object_id": "/instances/{{instanceId}}/providers/FoundationaLLM.Prompt/prompts/FoundationaLLM", "description": "Describes the persona and context for the FoundationaLLM agent, useful for answering questions about FoundationaLLM (FLLM).", diff --git a/docs/release-notes/breaking-changes.md b/docs/release-notes/breaking-changes.md index b64e9b8555..c6ddbf3406 100644 --- a/docs/release-notes/breaking-changes.md +++ b/docs/release-notes/breaking-changes.md @@ -3,9 +3,41 @@ > [!NOTE] > This section is for changes that are not yet released but will affect future releases. -## Breaking changes that will affect future releases +## Starting with 0.8.2 -### Starting with 0.8.0 +### Configuration changes + +The following settings are required: + +Name | Default value +--- | --- +`FoundationaLLM:APIEndpoints:CoreAPI:Configuration:AllowedUploadFileExtensions` | `c, cpp, cs, css, csv, doc, docx, git, html, java, jpeg, jpg, js, json, md, pdf, php, png, pptx, py, rb, sh, tar, tex, ts, txt, xlsx, xml, zip` +`FoundationaLLM:APIEndpoints:CoreAPI:Configuration:AzureOpenAIAssistantsFileSearchFileExtensions` | `c, cpp, cs, css, doc, docx, html, java, js, json, md, pdf, php, pptx, py, rb, sh, tex, ts, txt` + +The following settings are optional (they should not be set by default): + +Name | Default value +--- | --- +`FoundationaLLM:Instance:IdentitySubstitutionSecurityPrincipalId` | +`FoundationaLLM:Instance:IdentitySubstitutionUserPrincipalNamePattern` | `^fllm_load_test_user_\d{5}_\d{3}@solliance\.net$` + +>[!NOTE] +> The `FoundationaLLM:Instance:IdentitySubstitutionSecurityPrincipalId` and `FoundationaLLM:Instance:IdentitySubstitutionUserPrincipalNamePattern` settings are used for load testing purposes only. If set, their values must be replaced with the appropriate values for the specific Entra ID tenant. + +### Resource provider changes + +The following resource provider files must be renamed (if they already exist): + +Location | Old name | New name +--- | --- | --- +`resource-provider/FoundationaLLM.Agent` | `_agent-references.json` | `_resource-references.json` +`resource-provider/FoundationaLLM.AIModel` | `_ai-model-references.json` | `_resource-references.json` +`resource-provider/FoundationaLLM.Configuration` | `_api-endpoint-references.json` | `_resource-references.json` +`resource-provider/FoundationaLLM.DataSource` | `_data-source-references.json` | `_resource-references.json` +`resource-provider/FoundationaLLM.Prompt` | `_prompt-references.json` | `_resource-references.json` + + +## Starting with 0.8.0 Core API changes: @@ -31,7 +63,7 @@ Orchestration API changes: 1. All Gatekeeper API endpoints have been moved to the `/instances/{instanceId}` path. For example, the `/status` endpoint is now `/instances/{instanceId}/status`. 2. The `/orchestration/*` endpoints have been moved to `/instances/{instanceId}/completions/*`. ======= -#### New APIs +### New APIs **Gateway Adapter API** - requires the following configuration settings: @@ -48,7 +80,7 @@ Orchestration API changes: > [!NOTE] > These new APIs will be converted to use the new `APIEndpoint` artifacts. -#### Changes in app registration names +### Changes in app registration names API Name | Entra ID app registration name | Application ID URI | Scope name --- | --- | --- | --- @@ -58,7 +90,7 @@ Authorization API | `FoundationaLLM-Authorization-API` | `api://FoundationaLLM-A User Portal | `FoundationaLLM-Core-Portal` | `api://FoundationaLLM-Core-Portal` | N/A Management Portal | `FoundationaLLM-Management-Portal` | `api://FoundationaLLM-Management-Portal` | N/A -#### Changes in app configuration settings +### Changes in app configuration settings The `FoundationaLLM:APIs` and `FoundationaLLM:ExternalAPIs` configuration namespaces have been replaced with the `FoundationaLLM:APIEndpoints` configuration namespace. @@ -76,7 +108,7 @@ Two new configuration settings required by the new `FoundationaLLM.AzureOpenAI` - `FoundationaLLM:ResourceProviders:AzureOpenAI:Storage:AuthenticationType` - `FoundationaLLM:ResourceProviders:AzureOpenAI:Storage:AccountName` -### Pre-0.8.0 +## Pre-0.8.0 1. Vectorization resource stores use a unique collection name, `Resources`. They also add a new top-level property named `DefaultResourceName`. 2. The items in the `index_references` collection have a property incorrectly named `type` which was renamed to `index_entry_id`. diff --git a/src/FoundationaLLM.sln b/src/FoundationaLLM.sln index c74c812cd8..b1c6989a62 100644 --- a/src/FoundationaLLM.sln +++ b/src/FoundationaLLM.sln @@ -127,7 +127,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureOpenAI", "dotnet\Azure EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{B9ABEB53-19C8-4224-8820-CF2D82DCC559}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Examples.LoadTests", "..\tests\dotnet\Core.Examples.LoadTests\Core.Examples.LoadTests.csproj", "{ABA2F0F5-6372-4D7A-9B97-E1089C988B59}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core.Examples.LoadTests", "..\tests\dotnet\Core.Examples.LoadTests\Core.Examples.LoadTests.csproj", "{ABA2F0F5-6372-4D7A-9B97-E1089C988B59}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Conversation", "dotnet\Conversation\Conversation.csproj", "{EE94F312-488C-448B-92C9-20C4C1816F16}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -326,6 +328,10 @@ Global {ABA2F0F5-6372-4D7A-9B97-E1089C988B59}.Debug|Any CPU.Build.0 = Debug|Any CPU {ABA2F0F5-6372-4D7A-9B97-E1089C988B59}.Release|Any CPU.ActiveCfg = Release|Any CPU {ABA2F0F5-6372-4D7A-9B97-E1089C988B59}.Release|Any CPU.Build.0 = Release|Any CPU + {EE94F312-488C-448B-92C9-20C4C1816F16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE94F312-488C-448B-92C9-20C4C1816F16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE94F312-488C-448B-92C9-20C4C1816F16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE94F312-488C-448B-92C9-20C4C1816F16}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -388,6 +394,7 @@ Global {71DD0475-532B-49C0-8699-6552FF3A4A6E} = {2C948535-3001-4852-9686-492A23E9E356} {B9ABEB53-19C8-4224-8820-CF2D82DCC559} = {28E0E967-A94D-4820-8A61-0B71D3B2780F} {ABA2F0F5-6372-4D7A-9B97-E1089C988B59} = {B9ABEB53-19C8-4224-8820-CF2D82DCC559} + {EE94F312-488C-448B-92C9-20C4C1816F16} = {2C948535-3001-4852-9686-492A23E9E356} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FF5DE858-4B85-4EE8-8A6D-46E8E4FBA078} diff --git a/src/dotnet/AIModel/Models/AIModelReference.cs b/src/dotnet/AIModel/Models/AIModelReference.cs index 6e5f70159a..c0577de1bc 100644 --- a/src/dotnet/AIModel/Models/AIModelReference.cs +++ b/src/dotnet/AIModel/Models/AIModelReference.cs @@ -15,7 +15,7 @@ public class AIModelReference : ResourceReference /// The object type of the data source. /// [JsonIgnore] - public Type AIModelType => + public override Type ResourceType => Type switch { AIModelTypes.Basic => typeof(AIModelBase), diff --git a/src/dotnet/AIModel/ResourceProviders/AIModelResourceProviderService.cs b/src/dotnet/AIModel/ResourceProviders/AIModelResourceProviderService.cs index da0d51950b..4f0bbb60a6 100644 --- a/src/dotnet/AIModel/ResourceProviders/AIModelResourceProviderService.cs +++ b/src/dotnet/AIModel/ResourceProviders/AIModelResourceProviderService.cs @@ -7,17 +7,17 @@ using FoundationaLLM.Common.Exceptions; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models.Authorization; using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.Events; using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Models.ResourceProviders.Agent; using FoundationaLLM.Common.Models.ResourceProviders.AIModel; using FoundationaLLM.Common.Services.ResourceProviders; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System.Collections.Concurrent; -using System.Text; using System.Text.Json; namespace FoundationaLLM.AIModel.ResourceProviders @@ -50,159 +50,47 @@ public class AIModelResourceProviderService( loggerFactory.CreateLogger(), [ EventSetEventNamespaces.FoundationaLLM_ResourceProvider_AIModel - ]) + ], + useInternalReferencesStore: true) { /// protected override Dictionary GetResourceTypes() => AIModelResourceProviderMetadata.AllowedResourceTypes; - private ConcurrentDictionary _aiModelReferences; - /// protected override string _name => ResourceProviderNames.FoundationaLLM_AIModel; - private const string AIMODEL_REFERENCES_FILE_NAME = "_ai-model-references.json"; - private const string AIMODEL_REFERENCES_FILE_PATH = - $"/{ResourceProviderNames.FoundationaLLM_AIModel}/{AIMODEL_REFERENCES_FILE_NAME}"; /// - protected override async Task InitializeInternal() - { - _logger.LogInformation("Starting to initialize the {ResourceProvider} resource provider...", _name); - - if (await _storageService.FileExistsAsync(_storageContainerName, AIMODEL_REFERENCES_FILE_PATH, default)) - { - var fileContent = await _storageService.ReadFileAsync( - _storageContainerName, - AIMODEL_REFERENCES_FILE_PATH, - default); - - var resourceReferenceStore = - JsonSerializer.Deserialize>( - Encoding.UTF8.GetString(fileContent.ToArray())); - - _aiModelReferences = new ConcurrentDictionary( - resourceReferenceStore!.ResourceReferences.ToDictionary(r => r.Name)); - } - else - { - await _storageService.WriteFileAsync( - _storageContainerName, - AIMODEL_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(new ResourceReferenceList - { - ResourceReferences = [] - }), - default, - default); - } - - _logger.LogInformation("The {ResourceProvider} resource provider was successfully initialized.", _name); - } + protected override async Task InitializeInternal() => + await Task.CompletedTask; #region Resource provider support for Management API /// - protected override async Task GetResourcesAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + protected override async Task GetResourcesAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity, + ResourceProviderLoadOptions? options = null) => + resourcePath.MainResourceTypeName switch { - AIModelResourceTypeNames.AIModels => await LoadAIModels(resourcePath.ResourceTypeInstances[0], userIdentity), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + AIModelResourceTypeNames.AIModels => await LoadResources( + resourcePath.ResourceTypeInstances[0], + authorizationResult, + options ?? new ResourceProviderLoadOptions + { + IncludeRoles = resourcePath.IsResourceTypePath, + }), + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }; - #region Helpers for GetResourcesAsyncInternal - - private async Task>> LoadAIModels(ResourceTypeInstance instance, UnifiedUserIdentity userIdentity) - { - var aiModels = new List(); - - if (instance.ResourceId == null) - { - aiModels = (await Task.WhenAll(_aiModelReferences.Values - .Where(ar => !ar.Deleted) - .Select(ar => LoadAIModel(ar)))) - .Where(a => a != null) - .Select(a => a!) - .ToList(); - - } - else - { - AIModelBase? aiModel; - if (!_aiModelReferences.TryGetValue(instance.ResourceId, out var aiModelReference)) - { - aiModel = await LoadAIModel(null, instance.ResourceId); - if (aiModel != null) - aiModels.Add(aiModel); - } - else - { - if (aiModelReference.Deleted) - throw new ResourceProviderException( - $"Could not locate the {instance.ResourceId} aiModel resource.", - StatusCodes.Status404NotFound); - - aiModel = await LoadAIModel(aiModelReference); - if (aiModel != null) - aiModels.Add(aiModel); - } - } - return aiModels.Select(aiModel => new ResourceProviderGetResult() { Resource = aiModel, Actions = [], Roles = [] }).ToList(); - } - - /// - private async Task LoadAIModel(AIModelReference? aiModelReference, string? resourceId = null) - { - if (aiModelReference != null || !string.IsNullOrWhiteSpace(resourceId)) - { - aiModelReference ??= new AIModelReference - { - Name = resourceId!, - Type = AIModelTypes.Basic, - Filename = $"/{_name}/{resourceId}.json", - Deleted = false - }; - - - if (await _storageService.FileExistsAsync(_storageContainerName, aiModelReference.Filename, default)) - { - var fileContent = await _storageService.ReadFileAsync(_storageContainerName, aiModelReference.Filename, default); - var aiModel = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(fileContent.ToArray()), - aiModelReference.AIModelType, - base._serializerSettings) as AIModelBase - ?? throw new ResourceProviderException($"Failed to load the AI Model {aiModelReference.Name}.", - StatusCodes.Status400BadRequest); - - if (!string.IsNullOrWhiteSpace(resourceId)) - { - aiModelReference.Type = aiModel.Type!; - _aiModelReferences.AddOrUpdate(aiModelReference.Name, aiModelReference, (k, v) => aiModelReference); - } - - return aiModel; - } - - if (string.IsNullOrWhiteSpace(resourceId)) - { - // Remove the reference from the dictionary since the file does not exist. - _aiModelReferences.TryRemove(aiModelReference.Name, out _); - return null; - } - } - - throw new ResourceProviderException($"The {_name} resource provider could not locate a resource because of invalid resource identification parameters.", - StatusCodes.Status400BadRequest); - } - - #endregion - /// protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + resourcePath.MainResourceTypeName switch { AIModelResourceTypeNames.AIModels => await UpdateAIModel(resourcePath, serializedResource, userIdentity), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }; @@ -215,10 +103,7 @@ private async Task UpdateAIModel(ResourcePath reso ?? throw new ResourceProviderException("The object definition is invalid.", StatusCodes.Status400BadRequest); - if (_aiModelReferences.TryGetValue(aiModel.Name!, out var existingAIModelReference) - && existingAIModelReference!.Deleted) - throw new ResourceProviderException($"The AI model resource {existingAIModelReference.Name} cannot be added or updated.", - StatusCodes.Status400BadRequest); + var existingAIModelReference = await _resourceReferenceStore!.GetResourceReference(aiModel.Name); if (resourcePath.ResourceTypeInstances[0].ResourceId != aiModel.Name) throw new ResourceProviderException("The resource path does not match the object definition (name mismatch).", @@ -234,7 +119,7 @@ private async Task UpdateAIModel(ResourcePath reso aiModel.ObjectId = resourcePath.GetObjectId(_instanceSettings.Id, _name); - var validator = _resourceValidatorFactory.GetValidator(aiModelReference.AIModelType); + var validator = _resourceValidatorFactory.GetValidator(aiModelReference.ResourceType); if (validator is IValidator aiModelValidator) { var context = new ValidationContext(aiModel); @@ -246,98 +131,63 @@ private async Task UpdateAIModel(ResourcePath reso } } + UpdateBaseProperties(aiModel, userIdentity, isNew: existingAIModelReference == null); if (existingAIModelReference == null) - aiModel.CreatedBy = userIdentity.UPN; + await CreateResource(aiModelReference, aiModel); else - aiModel.UpdatedBy = userIdentity.UPN; - - await _storageService.WriteFileAsync( - _storageContainerName, - aiModelReference.Filename, - JsonSerializer.Serialize(aiModel, _serializerSettings), - default, - default); - - _aiModelReferences.AddOrUpdate(aiModelReference.Name, aiModelReference, (k, v) => aiModelReference); - - await _storageService.WriteFileAsync( - _storageContainerName, - AIMODEL_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(new ResourceReferenceList { ResourceReferences = _aiModelReferences.Values.ToList() }), - default, - default); + await SaveResource(existingAIModelReference, aiModel); return new ResourceProviderUpsertResult { - ObjectId = (aiModel as AIModelBase)!.ObjectId + ObjectId = aiModel!.ObjectId, + ResourceExists = existingAIModelReference != null }; } - private string GetFileExtension(string fileName) => - Path.GetExtension(fileName); - #endregion /// -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected override async Task ExecuteActionAsync(ResourcePath resourcePath, string serializedAction, UnifiedUserIdentity userIdentity) => throw new NotImplementedException(); -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + protected override async Task ExecuteActionAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + string serializedAction, + UnifiedUserIdentity userIdentity) => + resourcePath.ResourceTypeName switch + { + AIModelResourceTypeNames.AIModels => resourcePath.Action switch + { + ResourceProviderActions.CheckName => await CheckResourceName( + JsonSerializer.Deserialize(serializedAction)!), + ResourceProviderActions.Purge => await PurgeResource(resourcePath), + _ => throw new ResourceProviderException($"The action {resourcePath.Action} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest) + }, + _ => throw new ResourceProviderException() + }; /// protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) { - switch (resourcePath.ResourceTypeInstances.Last().ResourceType) + switch (resourcePath.ResourceTypeName) { case AIModelResourceTypeNames.AIModels: - await DeleteAIModel(resourcePath.ResourceTypeInstances); + await DeleteResource(resourcePath); break; default: - throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances.Last().ResourceType} is not supported by the {_name} resource provider.", + throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest); }; } - #region Helpers for DeleteResourceAsync - - private async Task DeleteAIModel(List instances) - { - if (_aiModelReferences.TryGetValue(instances.Last().ResourceId!, out var aiModelReference)) - { - if (!aiModelReference.Deleted) - { - aiModelReference.Deleted = true; - - await _storageService.WriteFileAsync( - _storageContainerName, - AIMODEL_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(new ResourceReferenceList { ResourceReferences = _aiModelReferences.Values.ToList() }), - default, - default); - } - } - else - { - throw new ResourceProviderException($"Could not locate the {instances.Last().ResourceId} aiModel resource.", - StatusCodes.Status404NotFound); - } - } - #endregion - #endregion + #region Resource provider strongly typed operations /// - protected override async Task GetResourceInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderOptions? options = null) where T : class - { - _aiModelReferences.TryGetValue(resourcePath.ResourceTypeInstances[0].ResourceId!, out var aiModelReference); - if (aiModelReference == null || aiModelReference.Deleted) - throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} was not found."); - - var aiModel = await LoadAIModel(aiModelReference); - return aiModel as T - ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} was not found."); - } + protected override async Task GetResourceAsyncInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) => + (await LoadResource(resourcePath.ResourceId!))!; + #endregion #region Event handling @@ -363,36 +213,40 @@ protected override async Task HandleEvents(EventSetEventArgs e) private async Task HandleAIModelResourceProviderEvent(CloudEvent e) { - if (string.IsNullOrWhiteSpace(e.Subject)) - return; + await Task.CompletedTask; + return; - var fileName = e.Subject.Split("/").Last(); + // Event handling is temporarily disabled until the updated event handling mechanism is implemented. - _logger.LogInformation("The file [{FileName}] managed by the [{ResourceProvider}] resource provider has changed and will be reloaded.", - fileName, _name); + //if (string.IsNullOrWhiteSpace(e.Subject)) + // return; - var aiModelReference = new AIModelReference - { - Name = Path.GetFileNameWithoutExtension(fileName), - Filename = $"/{_name}/{fileName}", - Type = nameof(AIModelBase), - Deleted = false - }; + //var fileName = e.Subject.Split("/").Last(); + + //_logger.LogInformation("The file [{FileName}] managed by the [{ResourceProvider}] resource provider has changed and will be reloaded.", + // fileName, _name); - var aiModel = await LoadAIModel(aiModelReference); - aiModelReference.Name = aiModel.Name; - aiModelReference.Type = aiModel.Type!; + //var aiModelReference = new AIModelReference + //{ + // Name = Path.GetFileNameWithoutExtension(fileName), + // Filename = $"/{_name}/{fileName}", + // Type = nameof(AIModelBase), + // Deleted = false + //}; - _aiModelReferences.AddOrUpdate( - aiModelReference.Name, - aiModelReference, - (k, v) => v); + //var aiModel = await LoadAIModel(aiModelReference); + //aiModelReference.Name = aiModel.Name; + //aiModelReference.Type = aiModel.Type!; - _logger.LogInformation("The aiModel reference for the [{AIModelName}] agent or type [{AIModelType}] was loaded.", - aiModelReference.Name, aiModelReference.Type); + //_aiModelReferences.AddOrUpdate( + // aiModelReference.Name, + // aiModelReference, + // (k, v) => v); + + //_logger.LogInformation("The aiModel reference for the [{AIModelName}] agent or type [{AIModelType}] was loaded.", + // aiModelReference.Name, aiModelReference.Type); } #endregion - } } diff --git a/src/dotnet/Agent/Models/Resources/AgentReference.cs b/src/dotnet/Agent/Models/Resources/AgentReference.cs index 7e0fabf421..8e1443d877 100644 --- a/src/dotnet/Agent/Models/Resources/AgentReference.cs +++ b/src/dotnet/Agent/Models/Resources/AgentReference.cs @@ -14,7 +14,7 @@ public class AgentReference : ResourceReference /// The object type of the agent. /// [JsonIgnore] - public Type AgentType => + public override Type ResourceType => Type switch { AgentTypes.Basic => typeof(AgentBase), diff --git a/src/dotnet/Agent/Models/Resources/AgentReferenceStore.cs b/src/dotnet/Agent/Models/Resources/AgentReferenceStore.cs deleted file mode 100644 index e7db0c70a9..0000000000 --- a/src/dotnet/Agent/Models/Resources/AgentReferenceStore.cs +++ /dev/null @@ -1,31 +0,0 @@ -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() - { - AgentReferences = [.. dictionary.Values] - }; - } -} diff --git a/src/dotnet/Agent/ResourceProviders/AgentResourceProviderService.cs b/src/dotnet/Agent/ResourceProviders/AgentResourceProviderService.cs index 9eea28f70d..d2f56834e9 100644 --- a/src/dotnet/Agent/ResourceProviders/AgentResourceProviderService.cs +++ b/src/dotnet/Agent/ResourceProviders/AgentResourceProviderService.cs @@ -2,13 +2,12 @@ using FluentValidation; using FoundationaLLM.Agent.Models.Resources; using FoundationaLLM.Common.Constants; -using FoundationaLLM.Common.Constants.Authorization; using FoundationaLLM.Common.Constants.Configuration; using FoundationaLLM.Common.Constants.ResourceProviders; using FoundationaLLM.Common.Exceptions; -using FoundationaLLM.Common.Extensions; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models.Authorization; using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.Events; using FoundationaLLM.Common.Models.ResourceProviders; @@ -19,9 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System.Collections.Concurrent; using System.Data; -using System.Text; using System.Text.Json; namespace FoundationaLLM.Agent.ResourceProviders @@ -52,158 +49,157 @@ public class AgentResourceProviderService( resourceValidatorFactory, serviceProvider, loggerFactory.CreateLogger(), - [ + eventNamespacesToSubscribe: [ EventSetEventNamespaces.FoundationaLLM_ResourceProvider_Agent - ]) + ], + useInternalReferencesStore: true) { /// protected override Dictionary GetResourceTypes() => AgentResourceProviderMetadata.AllowedResourceTypes; - 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_FILE_NAME}"; - /// protected override string _name => ResourceProviderNames.FoundationaLLM_Agent; /// - protected override async Task InitializeInternal() - { - _logger.LogInformation("Starting to initialize the {ResourceProvider} resource provider...", _name); + protected override async Task InitializeInternal() => + await Task.CompletedTask; - if (await _storageService.FileExistsAsync(_storageContainerName, AGENT_REFERENCES_FILE_PATH, default)) - { - var fileContent = await _storageService.ReadFileAsync(_storageContainerName, AGENT_REFERENCES_FILE_PATH, default); - var agentReferenceStore = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(fileContent.ToArray())); + #region Resource provider support for Management API - _agentReferences = new ConcurrentDictionary( - agentReferenceStore!.ToDictionary()); - } - else + /// + protected override async Task GetResourcesAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity, + ResourceProviderLoadOptions? options = null) => + resourcePath.MainResourceTypeName switch { - await _storageService.WriteFileAsync( - _storageContainerName, - AGENT_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(new AgentReferenceStore { AgentReferences = [] }), - default, - default); - } + AgentResourceTypeNames.Agents => await LoadResources( + resourcePath.ResourceTypeInstances[0], + authorizationResult, + options ?? new ResourceProviderLoadOptions + { + IncludeRoles = resourcePath.IsResourceTypePath, + }), + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest) + }; - _logger.LogInformation("The {ResourceProvider} resource provider was successfully initialized.", _name); - } + /// + protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => + resourcePath.MainResourceTypeName switch + { + AgentResourceTypeNames.Agents => await UpdateAgent(resourcePath, serializedResource, userIdentity), + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest) + }; - #region Resource provider support for Management API + /// + protected override async Task ExecuteActionAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + string serializedAction, + UnifiedUserIdentity userIdentity) => + resourcePath.ResourceTypeName switch + { + AgentResourceTypeNames.Agents => resourcePath.Action switch + { + ResourceProviderActions.CheckName => await CheckResourceName( + JsonSerializer.Deserialize(serializedAction)!), + ResourceProviderActions.Purge => await PurgeResource(resourcePath), + _ => throw new ResourceProviderException($"The action {resourcePath.Action} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest) + }, + _ => throw new ResourceProviderException() + }; /// - protected override async Task GetResourcesAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) + { + switch (resourcePath.ResourceTypeName) { - AgentResourceTypeNames.Agents => await LoadAgents(resourcePath.ResourceTypeInstances[0], userIdentity), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest) + case AgentResourceTypeNames.Agents: + await DeleteResource(resourcePath); + break; + default: + throw new ResourceProviderException( + $"The resource type {resourcePath.ResourceTypeName} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest); }; + } + + #endregion - #region Helpers for GetResourcesAsyncInternal + #region Resource provider strongly typed operations - private async Task>> LoadAgents(ResourceTypeInstance instance, UnifiedUserIdentity userIdentity) + /// + protected override async Task GetResourceAsyncInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) => + (await LoadResource(resourcePath.ResourceId!))!; + + #endregion + + #region Event handling + + /// + protected override async Task HandleEvents(EventSetEventArgs e) { - var agents = new List(); + _logger.LogInformation("{EventsCount} events received in the {EventsNamespace} events namespace.", + e.Events.Count, e.Namespace); - if (instance.ResourceId == null) - { - agents = (await Task.WhenAll(_agentReferences.Values - .Where(ar => !ar.Deleted) - .Select(ar => LoadAgent(ar)))) - .Where(agent => agent != null) - .Select(agent => agent!) - .ToList(); - } - else + switch (e.Namespace) { - AgentBase? agent; - if (!_agentReferences.TryGetValue(instance.ResourceId, out var agentReference)) - { - agent = await LoadAgent(null, instance.ResourceId); - if (agent != null) - agents.Add(agent); - } - else - { - if (agentReference.Deleted) - throw new ResourceProviderException($"Could not locate the {instance.ResourceId} agent resource.", - StatusCodes.Status404NotFound); - - agent = await LoadAgent(agentReference); - if (agent != null) - agents.Add(agent); - } + case EventSetEventNamespaces.FoundationaLLM_ResourceProvider_Agent: + foreach (var @event in e.Events) + await HandleAgentResourceProviderEvent(@event); + break; + default: + // Ignore sliently any event namespace that's of no interest. + break; } - return await _authorizationService.FilterResourcesByAuthorizableAction( - _instanceSettings.Id, userIdentity, agents, - AuthorizableActionNames.FoundationaLLM_Agent_Agents_Read); + await Task.CompletedTask; } - private async Task LoadAgent(AgentReference? agentReference, string? resourceId = null) + private async Task HandleAgentResourceProviderEvent(CloudEvent e) { - // agentReference is null for legacy agents - if (agentReference != null || !string.IsNullOrWhiteSpace(resourceId)) - { - agentReference ??= new AgentReference - { - Name = resourceId!, - Type = AgentTypes.Basic, - Filename = $"/{_name}/{resourceId}.json", - Deleted = false - }; - if (await _storageService.FileExistsAsync(_storageContainerName, agentReference.Filename, default)) - { - var fileContent = await _storageService.ReadFileAsync(_storageContainerName, agentReference.Filename, default); + await Task.CompletedTask; + return; - var agent = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(fileContent.ToArray()), - agentReference.AgentType, - _serializerSettings) as AgentBase - ?? throw new ResourceProviderException($"Failed to load the agent {agentReference.Name}.", - StatusCodes.Status400BadRequest); + // Event handling is temporarily disabled until the updated event handling mechanism is implemented. - if (!string.IsNullOrWhiteSpace(resourceId)) - { - // The agent file exists, but the agent reference is not in the dictionary. Update the dictionary with the missing reference. - agentReference.Type = agent.Type!; - _agentReferences.AddOrUpdate(agentReference.Name, agentReference, (k, v) => agentReference); - } + //if (string.IsNullOrWhiteSpace(e.Subject)) + // return; - return agent; - } + //var fileName = e.Subject.Split("/").Last(); - if (string.IsNullOrWhiteSpace(resourceId)) - { - // Remove the reference from the dictionary since the file does not exist. - _agentReferences.TryRemove(agentReference.Name, out _); - return null; - } - } + //_logger.LogInformation("The file [{FileName}] managed by the [{ResourceProvider}] resource provider has changed and will be reloaded.", + // fileName, _name); + + //var agentReference = new AgentReference + //{ + // Name = Path.GetFileNameWithoutExtension(fileName), + // Filename = $"/{_name}/{fileName}", + // Type = AgentTypes.Basic, + // Deleted = false + //}; + + //var getAgentResult = await LoadAgent(agentReference, null); + //agentReference.Name = getAgentResult.Name; + //agentReference.Type = getAgentResult.Type; - throw new ResourceProviderException($"The {_name} resource provider could not locate a resource because of invalid resource identification parameters.", - StatusCodes.Status400BadRequest); + //_agentReferences.AddOrUpdate( + // agentReference.Name, + // agentReference, + // (k, v) => v); + + //_logger.LogInformation("The agent reference for the [{AgentName}] agent or type [{AgentType}] was loaded.", + // agentReference.Name, agentReference.Type); } #endregion - /// - protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch - { - AgentResourceTypeNames.Agents => await UpdateAgent(resourcePath, serializedResource, userIdentity), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest) - }; - - #region Helpers for UpsertResourceAsync + #region Resource management private async Task UpdateAgent(ResourcePath resourcePath, string serializedAgent, UnifiedUserIdentity userIdentity) { @@ -211,10 +207,7 @@ private async Task UpdateAgent(ResourcePath resour ?? throw new ResourceProviderException("The object definition is invalid.", StatusCodes.Status400BadRequest); - if (_agentReferences.TryGetValue(agent.Name!, out var existingAgentReference) - && existingAgentReference!.Deleted) - throw new ResourceProviderException($"The agent resource {existingAgentReference.Name} cannot be added or updated.", - StatusCodes.Status400BadRequest); + var existingAgentReference = await _resourceReferenceStore!.GetResourceReference(agent.Name); if (resourcePath.ResourceTypeInstances[0].ResourceId != agent.Name) throw new ResourceProviderException("The resource path does not match the object definition (name mismatch).", @@ -230,7 +223,7 @@ private async Task UpdateAgent(ResourcePath resour agent.ObjectId = resourcePath.GetObjectId(_instanceSettings.Id, _name); - if ((agent is KnowledgeManagementAgent {Vectorization.DedicatedPipeline: true, InlineContext: false} kmAgent)) + if ((agent is KnowledgeManagementAgent { Vectorization.DedicatedPipeline: true, InlineContext: false } kmAgent)) { var result = await GetResourceProviderServiceByName(ResourceProviderNames.FoundationaLLM_Vectorization) .HandlePostAsync( @@ -244,7 +237,7 @@ private async Task UpdateAgent(ResourcePath resour TextPartitioningProfileObjectId = kmAgent.Vectorization.TextPartitioningProfileObjectId!, TextEmbeddingProfileObjectId = kmAgent.Vectorization.TextEmbeddingProfileObjectId!, IndexingProfileObjectId = kmAgent.Vectorization.IndexingProfileObjectIds[0]!, - TriggerType = (VectorizationPipelineTriggerType) kmAgent.Vectorization.TriggerType!, + TriggerType = (VectorizationPipelineTriggerType)kmAgent.Vectorization.TriggerType!, TriggerCronSchedule = kmAgent.Vectorization.TriggerCronSchedule }), userIdentity); @@ -257,7 +250,7 @@ private async Task UpdateAgent(ResourcePath resour StatusCodes.Status500InternalServerError); } - var validator = _resourceValidatorFactory.GetValidator(agentReference.AgentType); + var validator = _resourceValidatorFactory.GetValidator(agentReference.ResourceType); if (validator is IValidator agentValidator) { var context = new ValidationContext(agent); @@ -269,26 +262,11 @@ private async Task UpdateAgent(ResourcePath resour } } + UpdateBaseProperties(agent, userIdentity, isNew: existingAgentReference == null); if (existingAgentReference == null) - agent.CreatedBy = userIdentity.UPN; + await CreateResource(agentReference, agent); else - agent.UpdatedBy = userIdentity.UPN; - - await _storageService.WriteFileAsync( - _storageContainerName, - agentReference.Filename, - JsonSerializer.Serialize(agent, _serializerSettings), - default, - default); - - _agentReferences.AddOrUpdate(agentReference.Name, agentReference, (k, v) => agentReference); - - await _storageService.WriteFileAsync( - _storageContainerName, - AGENT_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(AgentReferenceStore.FromDictionary(_agentReferences.ToDictionary())), - default, - default); + await SaveResource(existingAgentReference, agent); return new ResourceProviderUpsertResult { @@ -298,180 +276,5 @@ await _storageService.WriteFileAsync( } #endregion - - /// -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected override async Task ExecuteActionAsync(ResourcePath resourcePath, string serializedAction, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances.Last().ResourceType switch - { - AgentResourceTypeNames.Agents => resourcePath.ResourceTypeInstances.Last().Action switch - { - ResourceProviderActions.CheckName => CheckAgentName(serializedAction), - ResourceProviderActions.Purge => await PurgeResource(resourcePath), - _ => throw new ResourceProviderException($"The action {resourcePath.ResourceTypeInstances.Last().Action} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest) - }, - _ => throw new ResourceProviderException() - }; -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - - #region Helpers for ExecuteActionAsync - - private ResourceNameCheckResult CheckAgentName(string serializedAction) - { - var resourceName = JsonSerializer.Deserialize(serializedAction); - return _agentReferences.Values.Any(ar => ar.Name.Equals(resourceName!.Name, StringComparison.OrdinalIgnoreCase)) - ? new ResourceNameCheckResult - { - Name = resourceName!.Name, - Type = resourceName.Type, - Status = NameCheckResultType.Denied, - Message = "A resource with the specified name already exists or was previously deleted and not purged." - } - : new ResourceNameCheckResult - { - Name = resourceName!.Name, - Type = resourceName.Type, - Status = NameCheckResultType.Allowed - }; - } - - private async Task PurgeResource(ResourcePath resourcePath) - { - var resourceName = resourcePath.ResourceTypeInstances.Last().ResourceId!; - if (_agentReferences.TryGetValue(resourceName, out var agentReference)) - { - if (agentReference.Deleted) - { - // Delete the resource file from storage. - await _storageService.DeleteFileAsync( - _storageContainerName, - agentReference.Filename, - default); - - // Remove this resource reference from the store. - _agentReferences.TryRemove(resourceName, out _); - - await _storageService.WriteFileAsync( - _storageContainerName, - AGENT_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(AgentReferenceStore.FromDictionary(_agentReferences.ToDictionary())), - default, - default); - - return new ResourceProviderActionResult(true); - } - else - { - throw new ResourceProviderException($"The {resourceName} agent resource is not soft-deleted and cannot be purged.", - StatusCodes.Status400BadRequest); - } - } - else - { - throw new ResourceProviderException($"Could not locate the {resourceName} agent resource.", - StatusCodes.Status404NotFound); - } - } - - #endregion - - /// - protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) - { - switch (resourcePath.ResourceTypeInstances.Last().ResourceType) - { - case AgentResourceTypeNames.Agents: - await DeleteAgent(resourcePath.ResourceTypeInstances); - break; - default: - throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances.Last().ResourceType} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest); - }; - } - - #region Helpers for DeleteResourceAsync - - private async Task DeleteAgent(List instances) - { - if (_agentReferences.TryGetValue(instances.Last().ResourceId!, out var agentReference)) - { - if (!agentReference.Deleted) - { - agentReference.Deleted = true; - - await _storageService.WriteFileAsync( - _storageContainerName, - AGENT_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(AgentReferenceStore.FromDictionary(_agentReferences.ToDictionary())), - default, - default); - } - } - else - { - throw new ResourceProviderException($"Could not locate the {instances.Last().ResourceId} agent resource.", - StatusCodes.Status404NotFound); - } - } - - #endregion - - #endregion - - #region Event handling - - /// - protected override async Task HandleEvents(EventSetEventArgs e) - { - _logger.LogInformation("{EventsCount} events received in the {EventsNamespace} events namespace.", - e.Events.Count, e.Namespace); - - switch (e.Namespace) - { - case EventSetEventNamespaces.FoundationaLLM_ResourceProvider_Agent: - foreach (var @event in e.Events) - await HandleAgentResourceProviderEvent(@event); - break; - default: - // Ignore sliently any event namespace that's of no interest. - break; - } - - await Task.CompletedTask; - } - - private async Task HandleAgentResourceProviderEvent(CloudEvent e) - { - if (string.IsNullOrWhiteSpace(e.Subject)) - return; - - var fileName = e.Subject.Split("/").Last(); - - _logger.LogInformation("The file [{FileName}] managed by the [{ResourceProvider}] resource provider has changed and will be reloaded.", - fileName, _name); - - var agentReference = new AgentReference - { - Name = Path.GetFileNameWithoutExtension(fileName), - Filename = $"/{_name}/{fileName}", - Type = AgentTypes.Basic, - Deleted = false - }; - - var getAgentResult = await LoadAgent(agentReference, null); - agentReference.Name = getAgentResult.Name; - agentReference.Type = getAgentResult.Type; - - _agentReferences.AddOrUpdate( - agentReference.Name, - agentReference, - (k, v) => v); - - _logger.LogInformation("The agent reference for the [{AgentName}] agent or type [{AgentType}] was loaded.", - agentReference.Name, agentReference.Type); - } - - #endregion } } diff --git a/src/dotnet/Attachments/ResourceProviders/AttachmentResourceProviderService.cs b/src/dotnet/Attachments/ResourceProviders/AttachmentResourceProviderService.cs index e5917d5e97..589c1de140 100644 --- a/src/dotnet/Attachments/ResourceProviders/AttachmentResourceProviderService.cs +++ b/src/dotnet/Attachments/ResourceProviders/AttachmentResourceProviderService.cs @@ -7,8 +7,9 @@ using FoundationaLLM.Common.Exceptions; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Authentication; -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Authorization; using FoundationaLLM.Common.Models.Configuration.Instance; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Events; using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Attachment; @@ -50,7 +51,7 @@ public class AttachmentResourceProviderService( [ EventSetEventNamespaces.FoundationaLLM_ResourceProvider_Attachment ], - useInternalStore: true) + useInternalReferencesStore: true) { /// protected override Dictionary GetResourceTypes() => @@ -64,136 +65,67 @@ protected override async Task InitializeInternal() => #region Resource provider support for Management API /// - protected override async Task GetResourcesAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + protected override async Task GetResourcesAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity, + ResourceProviderLoadOptions? options = null) => + resourcePath.MainResourceTypeName switch { - AttachmentResourceTypeNames.Attachments => await LoadAttachments(resourcePath.ResourceTypeInstances[0], userIdentity), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + AttachmentResourceTypeNames.Attachments => + // Attachments have a custom implementation for loading resources. + await LoadResources( + resourcePath.ResourceTypeInstances[0], + authorizationResult, + options ?? new ResourceProviderLoadOptions + { + IncludeRoles = resourcePath.IsResourceTypePath, + LoadContent = false + }, + LoadAttachment), + _ => + throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }; - #region Helpers for GetResourcesAsyncInternal - - private async Task>> LoadAttachments(ResourceTypeInstance instance, UnifiedUserIdentity userIdentity) - { - var attachments = new List(); - - if (instance.ResourceId == null) - { - var attachmentReferences = await _resourceReferenceStore!.GetAllResourceReferences(); - - attachments = (await Task.WhenAll(attachmentReferences - .Where(ar => !ar.Deleted) - .Select(ar => LoadAttachment(ar)))) - .Where(a => a != null) - .Select(a => a!) - .ToList(); - } - else - { - var attachmentReference = await _resourceReferenceStore!.GetResourceReference(instance.ResourceId); - AttachmentFile? attachment; - - if (attachmentReference != null) - { - attachment = await LoadAttachment(attachmentReference); - if (attachment != null) - attachments.Add(attachment); - } - } - return attachments.Select(attachment => new ResourceProviderGetResult() { Resource = attachment, Actions = [], Roles = [] }).ToList(); - } - - private async Task LoadAttachment(AttachmentReference attachmentReference, bool loadContent = false) - { - var attachmentFile = new AttachmentFile - { - ObjectId = attachmentReference.ObjectId, - Name = attachmentReference.Name, - OriginalFileName = attachmentReference.OriginalFilename, - Type = attachmentReference.Type, - Path = $"{_storageContainerName}{attachmentReference.Filename}", - ContentType = attachmentReference.ContentType, - SecondaryProvider = attachmentReference.SecondaryProvider - }; - - if (loadContent) - { - var fileContent = await _storageService.ReadFileAsync( - _storageContainerName, - attachmentReference.Filename, - default); - attachmentFile.Content = fileContent.ToArray(); - } - - return attachmentFile; - } - - #endregion - /// - protected override Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => null; - - /// -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected override async Task ExecuteActionAsync(ResourcePath resourcePath, string serializedAction, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances.Last().ResourceType switch + protected override async Task ExecuteActionAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + string serializedAction, + UnifiedUserIdentity userIdentity) => + resourcePath.ResourceTypeName switch { - AttachmentResourceTypeNames.Attachments => resourcePath.ResourceTypeInstances.Last().Action switch + AttachmentResourceTypeNames.Attachments => resourcePath.Action switch { - ResourceProviderActions.Filter => await Filter(serializedAction), - _ => throw new ResourceProviderException($"The action {resourcePath.ResourceTypeInstances.Last().Action} is not supported by the {_name} resource provider.", + ResourceProviderActions.Filter => + // Attachments have a custom implementation for loading resources. + await FilterResources( + resourcePath, + JsonSerializer.Deserialize(serializedAction)!, + authorizationResult, + new ResourceProviderLoadOptions + { + LoadContent = false, + IncludeRoles = false + }, + LoadAttachment), + _ => throw new ResourceProviderException($"The action {resourcePath.Action} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }, _ => throw new ResourceProviderException() }; -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - - #region Helpers for ExecuteActionAsync - - private async Task> Filter(string serializedAction) - { - var resourceFilter = JsonSerializer.Deserialize(serializedAction) - ?? throw new ResourceProviderException("The object definition is invalid. Please provide a resource filter.", - StatusCodes.Status400BadRequest); - if (resourceFilter.ObjectIDs is {Count: > 0}) - { - var resourceNames = resourceFilter.ObjectIDs - .Select(id => this.GetResourcePath(id, false).ResourceTypeInstances.Last().ResourceId!) - .ToList(); - var filteredReferences = - await _resourceReferenceStore!.GetResourceReferences(resourceNames); - - return filteredReferences.Select(r => new AttachmentDetail - { - ObjectId = r.ObjectId, - DisplayName = r.OriginalFilename, - ContentType = r.ContentType - }).ToList(); - - } - - var allResourceReferences = await _resourceReferenceStore!.GetAllResourceReferences(); - return allResourceReferences.Select(r => new AttachmentDetail - { - ObjectId = r.ObjectId, - DisplayName = r.OriginalFilename, - ContentType = r.ContentType - }).ToList(); - } - - #endregion /// protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) { - switch (resourcePath.ResourceTypeInstances.Last().ResourceType) + switch (resourcePath.ResourceTypeName) { case AttachmentResourceTypeNames.Attachments: - await DeleteResource(resourcePath.ResourceTypeInstances.Last().ResourceId!); + await DeleteResource(resourcePath); break; default: - throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances.Last().ResourceType} is not supported by the {_name} resource provider.", + throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest); }; } @@ -203,13 +135,13 @@ protected override async Task DeleteResourceAsync(ResourcePath resourcePath, Uni #region Resource provider strongly typed operations /// - protected override async Task GetResourceInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderOptions? options = null) where T : class + protected override async Task GetResourceAsyncInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) where T : class { var attachmentReference = await _resourceReferenceStore!.GetResourceReference(resourcePath.ResourceTypeInstances[0].ResourceId!) - ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} was not found."); + ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.MainResourceTypeName} was not found."); return (await LoadAttachment(attachmentReference, loadContent: options?.LoadContent ?? false)) as T - ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} could not be loaded."); + ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.MainResourceTypeName} could not be loaded."); } /// @@ -252,6 +184,31 @@ protected override async Task HandleEvents(EventSetEventArgs e) #region Resource management + private async Task LoadAttachment(AttachmentReference attachmentReference, bool loadContent = false) + { + var attachmentFile = new AttachmentFile + { + ObjectId = attachmentReference.ObjectId, + Name = attachmentReference.Name, + OriginalFileName = attachmentReference.OriginalFilename, + Type = attachmentReference.Type, + Path = $"{_storageContainerName}{attachmentReference.Filename}", + ContentType = attachmentReference.ContentType, + SecondaryProvider = attachmentReference.SecondaryProvider + }; + + if (loadContent) + { + var fileContent = await _storageService.ReadFileAsync( + _storageContainerName, + attachmentReference.Filename, + default); + attachmentFile.Content = fileContent.ToArray(); + } + + return attachmentFile; + } + private async Task UpdateAttachment(ResourcePath resourcePath, AttachmentFile attachment) { if (resourcePath.ResourceTypeInstances[0].ResourceId != attachment.Name) @@ -293,7 +250,8 @@ await CreateResource( return new ResourceProviderUpsertResult { - ObjectId = (attachment as AttachmentFile)!.ObjectId + ObjectId = (attachment as AttachmentFile)!.ObjectId, + ResourceExists = false }; } diff --git a/src/dotnet/Authorization/Interfaces/IAuthorizationCore.cs b/src/dotnet/Authorization/Interfaces/IAuthorizationCore.cs index 45b4d7fa67..4de455bf57 100644 --- a/src/dotnet/Authorization/Interfaces/IAuthorizationCore.cs +++ b/src/dotnet/Authorization/Interfaces/IAuthorizationCore.cs @@ -8,14 +8,6 @@ namespace FoundationaLLM.Authorization.Interfaces /// public interface IAuthorizationCore { - /// - /// Processes an authorization request. - /// - /// The FoundationaLLM instance id. - /// The containing the details of the authorization request. - /// An indicating whether the requested authorization was successfull or not for each resource path. - ActionAuthorizationResult ProcessAuthorizationRequest(string instanceId, ActionAuthorizationRequest authorizationRequest); - /// /// Checks if a specified security principal is allowed to process authorization requests. /// @@ -24,13 +16,21 @@ public interface IAuthorizationCore /// True if the security principal is allowed to process authorization requests. bool AllowAuthorizationRequestsProcessing(string instanceId, string securityPrincipalId); + /// + /// Processes an authorization request. + /// + /// The FoundationaLLM instance id. + /// The containing the details of the authorization request. + /// An indicating whether the requested authorization was successfull or not for each resource path. + ActionAuthorizationResult ProcessAuthorizationRequest(string instanceId, ActionAuthorizationRequest authorizationRequest); + /// /// Creates a role assignment for a specified security principal. /// /// The FoundationaLLM instance identifier. /// The role assignment request. /// The role assignment result. - Task CreateRoleAssignment(string instanceId, RoleAssignmentRequest roleAssignmentRequest); + Task CreateRoleAssignment(string instanceId, RoleAssignmentRequest roleAssignmentRequest); /// /// Revokes a role from an Entra ID user or group. @@ -38,16 +38,9 @@ public interface IAuthorizationCore /// The FoundationaLLM instance identifier. /// The role assignment object identifier. /// The role assignment result. - Task RevokeRoleAssignment(string instanceId, string roleAssignment); - - /// - /// Returns a list of role names and a list of allowed actions for the specified scope. - /// - /// The FoundationaLLM instance identifier. - /// The get roles with actions request. - /// The get roles and actions result. - Dictionary ProcessRoleAssignmentsWithActionsRequest(string instanceId, RoleAssignmentsWithActionsRequest request); + Task DeleteRoleAssignment(string instanceId, string roleAssignment); + /// /// Returns a list of role assignments for the specified instance and resource path. /// diff --git a/src/dotnet/Authorization/ResourceProviders/AuthorizationResourceProviderService.cs b/src/dotnet/Authorization/ResourceProviders/AuthorizationResourceProviderService.cs index 1308c8df0e..08ea1679d9 100644 --- a/src/dotnet/Authorization/ResourceProviders/AuthorizationResourceProviderService.cs +++ b/src/dotnet/Authorization/ResourceProviders/AuthorizationResourceProviderService.cs @@ -52,11 +52,17 @@ protected override async Task InitializeInternal() => #region Resource provider support for Management API /// - protected override async Task GetResourcesAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + protected override async Task GetResourcesAsync( +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity, + ResourceProviderLoadOptions? options = null) => + resourcePath.MainResourceTypeName switch { AuthorizationResourceTypeNames.RoleDefinitions => LoadRoleDefinitions(resourcePath.ResourceTypeInstances[0]), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }; @@ -79,10 +85,10 @@ private static List LoadRoleDefinitions(ResourceTypeInstance ins /// protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + resourcePath.MainResourceTypeName switch { AuthorizationResourceTypeNames.RoleAssignments => await UpdateRoleAssignments(resourcePath, serializedResource, userIdentity), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }; @@ -109,7 +115,7 @@ private async Task UpdateRoleAssignments(ResourceP StatusCodes.Status400BadRequest); } - var roleAssignmentResult = await _authorizationService.ProcessRoleAssignmentRequest( + var roleAssignmentResult = await _authorizationService.CreateRoleAssignment( _instanceSettings.Id, new RoleAssignmentRequest() { @@ -127,7 +133,8 @@ private async Task UpdateRoleAssignments(ResourceP if (roleAssignmentResult.Success) return new ResourceProviderUpsertResult { - ObjectId = roleAssignment.ObjectId + ObjectId = roleAssignment.ObjectId, + ResourceExists = false }; throw new ResourceProviderException("The role assignment failed."); @@ -138,30 +145,32 @@ private async Task UpdateRoleAssignments(ResourceP /// protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) { - switch (resourcePath.ResourceTypeInstances.Last().ResourceType) + switch (resourcePath.ResourceTypeName) { case AuthorizationResourceTypeNames.RoleAssignments: - await _authorizationService.RevokeRoleAssignment( + await _authorizationService.DeleteRoleAssignment( _instanceSettings.Id, - resourcePath.ResourceTypeInstances.Last().ResourceId!, + resourcePath.ResourceId!, userIdentity); break; default: - throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances.Last().ResourceType} is not supported by the {_name} resource provider.", + throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest); }; } - #endregion - /// - protected override async Task ExecuteActionAsync(ResourcePath resourcePath, string serializedAction, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances.Last().ResourceType switch + protected override async Task ExecuteActionAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + string serializedAction, + UnifiedUserIdentity userIdentity) => + resourcePath.ResourceTypeName switch { - AuthorizationResourceTypeNames.RoleAssignments => resourcePath.ResourceTypeInstances.Last().Action switch + AuthorizationResourceTypeNames.RoleAssignments => resourcePath.Action switch { ResourceProviderActions.Filter => await FilterRoleAssignments(resourcePath.ResourceTypeInstances[0], serializedAction, userIdentity), - _ => throw new ResourceProviderException($"The action {resourcePath.ResourceTypeInstances.Last().Action} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The action {resourcePath.Action} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }, _ => throw new ResourceProviderException() @@ -177,16 +186,10 @@ private async Task>> FilterRoleAs throw new ResourceProviderException("Invalid scope. Unable to retrieve role assignments."); else { - var roleAssignments = new List(); - var roleAssignmentObjects = await _authorizationService.GetRoleAssignments( - _instanceSettings.Id, queryParameters, userIdentity); - - foreach (var obj in roleAssignmentObjects) - { - var roleAssignment = JsonSerializer.Deserialize(obj.ToString()!)!; - if (!roleAssignment.Deleted) - roleAssignments.Add(roleAssignment); - } + var roleAssignments = (await _authorizationService.GetRoleAssignments( + _instanceSettings.Id, queryParameters, userIdentity)) + .Where(ra => !ra.Deleted) + .ToList(); if (instance.ResourceId != null) { @@ -199,10 +202,12 @@ private async Task>> FilterRoleAs roleAssignments = [roleAssignment]; } - return roleAssignments.Select(x => new ResourceProviderGetResult() { Resource = x, Actions = [], Roles = [] }).ToList(); + return roleAssignments.Select(x => new ResourceProviderGetResult() { Resource = x, Roles = [] }).ToList(); } } #endregion + + #endregion } } diff --git a/src/dotnet/Authorization/Services/AuthorizationCore.cs b/src/dotnet/Authorization/Services/AuthorizationCore.cs index 8e38d86284..53c08d5b98 100644 --- a/src/dotnet/Authorization/Services/AuthorizationCore.cs +++ b/src/dotnet/Authorization/Services/AuthorizationCore.cs @@ -32,7 +32,7 @@ public class AuthorizationCore : IAuthorizationCore private const string ROLE_ASSIGNMENTS_CONTAINER_NAME = "role-assignments"; private bool _initialized = false; - private readonly object _syncRoot = new(); + private readonly SemaphoreSlim _syncRoot = new SemaphoreSlim(1, 1); /// /// Creates a new instance of the class. @@ -116,10 +116,43 @@ await _storageService.WriteFileAsync( } } + /// + public bool AllowAuthorizationRequestsProcessing(string instanceId, string securityPrincipalId) + { + var resourcePath = $"/instances/{instanceId}/providers/{ResourceProviderNames.FoundationaLLM_Authorization}/{AuthorizationResourceTypeNames.RoleAssignments}"; + _ = ResourcePath.TryParse( + resourcePath, + [ResourceProviderNames.FoundationaLLM_Authorization], + AuthorizationResourceProviderMetadata.AllowedResourceTypes, + false, + out ResourcePath? parsedResourcePath); + var result = ProcessAuthorizationRequestForResourcePath(parsedResourcePath!, new ActionAuthorizationRequest + { + Action = AuthorizableActionNames.FoundationaLLM_Authorization_RoleAssignments_Read, + ResourcePaths = [resourcePath], + ExpandResourceTypePaths = false, + IncludeRoles = false, + UserContext = new UserAuthorizationContext + { + SecurityPrincipalId = securityPrincipalId, + UserPrincipalName = securityPrincipalId, + SecurityGroupIds = [] + } + }); + return result.Authorized; + } + /// public ActionAuthorizationResult ProcessAuthorizationRequest(string instanceId, ActionAuthorizationRequest authorizationRequest) { - var authorizationResults = authorizationRequest.ResourcePaths.Distinct().ToDictionary(rp => rp, auth => false); + var authorizationResults = authorizationRequest.ResourcePaths.Distinct().ToDictionary(rp => rp, rp => new ResourcePathAuthorizationResult + { + ResourceName = string.Empty, + ResourcePath = rp, + Authorized = false, + Roles = [], + SubordinateResourcePathsAuthorizationResults = [] + }); var invalidResourcePaths = new List(); try @@ -140,29 +173,32 @@ public ActionAuthorizationResult ProcessAuthorizationRequest(string instanceId, { try { - var resourcePath = ResourcePathUtils.ParseForAuthorizationRequestResourcePath(rp, _settings.InstanceIds); + var parsedResourcePath = ResourcePathUtils.ParseForAuthorizationRequestResourcePath(rp, _settings.InstanceIds); - if (string.IsNullOrWhiteSpace(resourcePath.InstanceId) - || resourcePath.InstanceId.ToLower().CompareTo(instanceId.ToLower()) != 0) + if (string.IsNullOrWhiteSpace(parsedResourcePath.InstanceId) + || StringComparer.OrdinalIgnoreCase.Compare(parsedResourcePath.InstanceId, instanceId) != 0) { _logger.LogError("The instance id from the controller route and the instance id from the authorization request do not match."); - authorizationResults[rp] = false; invalidResourcePaths.Add(rp); } - - authorizationResults[rp] = ActionAllowed(resourcePath, new ActionAuthorizationRequest() + else { - Action = authorizationRequest.Action, - ResourcePaths = [rp], - PrincipalId = authorizationRequest.PrincipalId, - SecurityGroupIds = authorizationRequest.SecurityGroupIds - }); + authorizationResults[rp] = ProcessAuthorizationRequestForResourcePath(parsedResourcePath, new ActionAuthorizationRequest() + { + Action = authorizationRequest.Action, + ResourcePaths = [rp], + ExpandResourceTypePaths = parsedResourcePath.IsResourceTypePath + ? authorizationRequest.ExpandResourceTypePaths + : false, + IncludeRoles = authorizationRequest.IncludeRoles, + UserContext = authorizationRequest.UserContext + }); + } } catch (Exception ex) { // If anything goes wrong, we default to denying the request on that particular resource. _logger.LogWarning(ex, "The authorization core failed to process the authorization request for: {ResourcePath}.", rp); - authorizationResults[rp] = false; invalidResourcePaths.Add(rp); } } @@ -180,78 +216,69 @@ public ActionAuthorizationResult ProcessAuthorizationRequest(string instanceId, } /// - public bool AllowAuthorizationRequestsProcessing(string instanceId, string securityPrincipalId) - { - var resourcePath = $"/instances/{instanceId}/providers/{ResourceProviderNames.FoundationaLLM_Authorization}/{AuthorizationResourceTypeNames.RoleAssignments}"; - _ = ResourcePath.TryParse( - resourcePath, - [ResourceProviderNames.FoundationaLLM_Authorization], - AuthorizationResourceProviderMetadata.AllowedResourceTypes, - false, - out ResourcePath? parsedResourcePath); - return ActionAllowed(parsedResourcePath!, new ActionAuthorizationRequest - { - Action = AuthorizableActionNames.FoundationaLLM_Authorization_RoleAssignments_Read, - PrincipalId = securityPrincipalId, - ResourcePaths = [resourcePath] - }); - } - - - /// - public async Task CreateRoleAssignment(string instanceId, RoleAssignmentRequest roleAssignmentRequest) + public async Task CreateRoleAssignment(string instanceId, RoleAssignmentRequest roleAssignmentRequest) { var roleAssignmentStoreFile = $"/{instanceId.ToLower()}.json"; if (await _storageService.FileExistsAsync(ROLE_ASSIGNMENTS_CONTAINER_NAME, roleAssignmentStoreFile, default)) { - var fileContent = await _storageService.ReadFileAsync(ROLE_ASSIGNMENTS_CONTAINER_NAME, roleAssignmentStoreFile, default); - var roleAssignmentStore = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(fileContent.ToArray())); - if (roleAssignmentStore != null) + try { - var exists = roleAssignmentStore.RoleAssignments.Any(x => x.PrincipalId == roleAssignmentRequest.PrincipalId - && x.Scope == roleAssignmentRequest.Scope - && x.RoleDefinitionId == roleAssignmentRequest.RoleDefinitionId); - if (!exists) + await _syncRoot.WaitAsync(); + + var fileContent = await _storageService.ReadFileAsync(ROLE_ASSIGNMENTS_CONTAINER_NAME, roleAssignmentStoreFile, default); + var roleAssignmentStore = JsonSerializer.Deserialize( + Encoding.UTF8.GetString(fileContent.ToArray())); + if (roleAssignmentStore != null) { - var roleAssignment = new RoleAssignment() + var exists = roleAssignmentStore.RoleAssignments.Any(x => x.PrincipalId == roleAssignmentRequest.PrincipalId + && x.Scope == roleAssignmentRequest.Scope + && x.RoleDefinitionId == roleAssignmentRequest.RoleDefinitionId); + if (!exists) { - Type = $"{ResourceProviderNames.FoundationaLLM_Authorization}/{AuthorizationResourceTypeNames.RoleAssignments}", - Name = roleAssignmentRequest.Name, - Description = roleAssignmentRequest.Description, - ObjectId = roleAssignmentRequest.ObjectId, - PrincipalId = roleAssignmentRequest.PrincipalId, - PrincipalType = roleAssignmentRequest.PrincipalType, - RoleDefinitionId = roleAssignmentRequest.RoleDefinitionId, - Scope = roleAssignmentRequest.Scope, - CreatedBy = roleAssignmentRequest.CreatedBy - }; - - roleAssignmentStore.RoleAssignments.Add(roleAssignment); - _roleAssignmentStores.AddOrUpdate(instanceId, roleAssignmentStore, (k, v) => roleAssignmentStore); - roleAssignmentStore.EnrichRoleAssignments(); - _roleAssignmentCaches[instanceId].AddOrUpdateRoleAssignment(roleAssignment); + var roleAssignment = new RoleAssignment() + { + Type = $"{ResourceProviderNames.FoundationaLLM_Authorization}/{AuthorizationResourceTypeNames.RoleAssignments}", + Name = roleAssignmentRequest.Name, + Description = roleAssignmentRequest.Description, + ObjectId = roleAssignmentRequest.ObjectId, + PrincipalId = roleAssignmentRequest.PrincipalId, + PrincipalType = roleAssignmentRequest.PrincipalType, + RoleDefinitionId = roleAssignmentRequest.RoleDefinitionId, + Scope = roleAssignmentRequest.Scope, + CreatedBy = roleAssignmentRequest.CreatedBy + }; + + roleAssignmentStore.RoleAssignments.Add(roleAssignment); + _roleAssignmentStores.AddOrUpdate(instanceId, roleAssignmentStore, (k, v) => roleAssignmentStore); + roleAssignmentStore.EnrichRoleAssignments(); + _roleAssignmentCaches[instanceId].AddOrUpdateRoleAssignment(roleAssignment); - await _storageService.WriteFileAsync( - ROLE_ASSIGNMENTS_CONTAINER_NAME, - roleAssignmentStoreFile, - JsonSerializer.Serialize(roleAssignmentStore), - default, - default); + await _storageService.WriteFileAsync( + ROLE_ASSIGNMENTS_CONTAINER_NAME, + roleAssignmentStoreFile, + JsonSerializer.Serialize(roleAssignmentStore), + default, + default); - return new RoleAssignmentResult() { Success = true }; + return new RoleAssignmentOperationResult() { Success = true }; + } } } + finally + { + _syncRoot.Release(); + } } - return new RoleAssignmentResult() { Success = false }; + return new RoleAssignmentOperationResult() { Success = false }; } /// - public async Task RevokeRoleAssignment(string instanceId, string roleAssignment) + public async Task DeleteRoleAssignment(string instanceId, string roleAssignment) { - var existingRoleAssignment = _roleAssignmentStores[instanceId].RoleAssignments.SingleOrDefault(x => x.Name == roleAssignment); + var existingRoleAssignment = _roleAssignmentStores[instanceId].RoleAssignments + .SingleOrDefault(x => x.Name == roleAssignment); if (existingRoleAssignment != null) { _roleAssignmentCaches[instanceId].RemoveRoleAssignment(roleAssignment); @@ -264,10 +291,10 @@ await _storageService.WriteFileAsync( default, default); - return new RoleAssignmentResult() { Success = true }; + return new RoleAssignmentOperationResult() { Success = true }; } - return new RoleAssignmentResult() { Success = false }; + return new RoleAssignmentOperationResult() { Success = false }; } /// @@ -284,89 +311,132 @@ public List GetRoleAssignments(string instanceId, RoleAssignment .Where(ra => resourcePath.IncludesResourcePath(ra.ScopeResourcePath!)) .ToList(); } - - /// - public Dictionary ProcessRoleAssignmentsWithActionsRequest(string instanceId, RoleAssignmentsWithActionsRequest request) + + private ResourcePathAuthorizationResult ProcessAuthorizationRequestForResourcePath( + ResourcePath resourcePath, + ActionAuthorizationRequest authorizationRequest) { - var result = request.Scopes.Distinct().ToDictionary(scp => scp, res => new RoleAssignmentsWithActionsResult() { Actions = [], Roles = [] }); - - foreach (var scope in request.Scopes) + var result = new ResourcePathAuthorizationResult { - try - { - _ = ResourcePath.TryParseResourceProvider(scope, out string? resourceProdiver); - var requestScope = ResourcePathUtils.ParseForAuthorizationRequestResourcePath(scope, _settings.InstanceIds); - - if (string.IsNullOrWhiteSpace(requestScope.InstanceId) || requestScope.InstanceId.ToLower().CompareTo(instanceId.ToLower()) != 0) - { - _logger.LogError("The instance id from the controller route and the instance id from the authorization request do not match."); - } - else - { - var roleAssignments = _roleAssignmentStores[instanceId].RoleAssignments.Where(x => x.PrincipalId == request.PrincipalId || request.SecurityGroupIds.Contains(x.PrincipalId)).ToList(); - foreach (var ra in roleAssignments) - { - if (scope.Contains(ra.Scope)) - { - result[scope].Actions.AddRange(ra.AllowedActions.Where(x => x.Contains(resourceProdiver!)).ToList()); - result[scope].Roles.Add(ra.RoleDefinition!.DisplayName!); - } - } - - // Duplicated actions might exist when a pricipal has multiple roles with overlapping permissions. - result[scope].Actions = result[scope].Actions.Distinct().ToList(); - result[scope].Roles = result[scope].Roles.Distinct().ToList(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an issue while processing the get roles with actions request for {Scope}.", scope); - } - } - - return result; - } + ResourceName = resourcePath.MainResourceId, + ResourcePath = resourcePath.RawResourcePath, + Authorized = false, + Roles = [], + SubordinateResourcePathsAuthorizationResults = [] + }; - private bool ActionAllowed(ResourcePath resourcePath, ActionAuthorizationRequest authorizationRequest) - { // Get cache associated with the instance id. if (_roleAssignmentCaches.TryGetValue(resourcePath.InstanceId!, out var roleAssignmentCache)) { + List allRoleAssignments = []; + // Combine the principal id and security group ids into one list. - var objectIds = new List { authorizationRequest.PrincipalId }; - if (authorizationRequest.SecurityGroupIds != null) - objectIds.AddRange(authorizationRequest.SecurityGroupIds); + var securityPrincipalIds = new List { authorizationRequest.UserContext.SecurityPrincipalId }; + if (authorizationRequest.UserContext.SecurityGroupIds != null) + securityPrincipalIds.AddRange(authorizationRequest.UserContext.SecurityGroupIds); - foreach (var objectId in objectIds) + foreach (var securityPrincipalId in securityPrincipalIds) { - // Retrieve all role assignments associated with the id. - var roleAssignments = roleAssignmentCache.GetRoleAssignments(objectId); + // Retrieve all role assignments associated with the security principal id. + var roleAssignments = roleAssignmentCache.GetRoleAssignments(securityPrincipalId); foreach (var roleAssignment in roleAssignments) { // Retrieve the role definition object if (RoleDefinitions.All.TryGetValue(roleAssignment.RoleDefinitionId, out var roleDefinition)) { - // Check if the scope of the role assignment includes the resource. + // Check if the scope of the role assignment covers the resource. // Check if the actions of the role definition include the requested action. if (resourcePath.IncludesResourcePath(roleAssignment.ScopeResourcePath!) && roleAssignment.AllowedActions.Contains(authorizationRequest.Action)) { - return true; + result.Authorized = true; + + // If we are not asked to include roles and not asked to expand resource paths, + // we can return immediately (this is the most common case). + // Otherwise, we need to go through the entire list of security principals and their role assignments, + // to include collect all the roles and/or all the subordinate authorized resource paths. + if (!authorizationRequest.IncludeRoles + && !authorizationRequest.ExpandResourceTypePaths) + return result; } } else _logger.LogWarning("The role assignment {RoleAssignmentName} references the role definition {RoleDefinitionId} which is invalid.", roleAssignment.Name, roleAssignment.RoleDefinitionId); } + + allRoleAssignments.AddRange(roleAssignments); } - } - _logger.LogWarning("The action {ActionName} is not allowed on the resource {ResourcePath} for the principal {PrincipalId}.", - authorizationRequest.Action, - resourcePath, - authorizationRequest.PrincipalId); + if (!result.Authorized + && !resourcePath.IsResourceTypePath) + { + _logger.LogWarning("The action {ActionName} is not allowed on the resource {ResourcePath} for the principal {PrincipalId}.", + authorizationRequest.Action, + resourcePath, + authorizationRequest.UserContext.SecurityPrincipalId); + } + + if (authorizationRequest.IncludeRoles + && allRoleAssignments.Count > 0) + { + // Include the display names of the roles in the result. + result.Roles = allRoleAssignments + .Select(ra => ra.RoleDefinition!.DisplayName!) + .Distinct() + .ToList(); + } + + if (authorizationRequest.ExpandResourceTypePaths + && resourcePath.IsResourceTypePath) + { + Dictionary subordinateResults = []; + + // If the resource path is a resource type path, we need to expand the resource type path. + // We will check all the resource paths that are authorized and add them to the list of subordinate authorized resource paths. + foreach (var roleAssignment in allRoleAssignments) + { + // Considering only role assignments for resource paths that are subordinate to the requested resource path. + if (roleAssignment.ScopeResourcePath!.IncludesResourcePath(resourcePath, allowEqual: false)) + { + // Keep track of all role assignments until the end, when we know for sure whether the action is authorized or not. + if (!subordinateResults.ContainsKey( + roleAssignment.ScopeResourcePath!.MainResourceId!)) + { + subordinateResults.Add( + roleAssignment.ScopeResourcePath!.MainResourceId!, + new ResourcePathAuthorizationResult + { + ResourceName = roleAssignment.ScopeResourcePath!.MainResourceId, + ResourcePath = roleAssignment.ScopeResourcePath!.RawResourcePath, + Authorized = false, + Roles = [], + SubordinateResourcePathsAuthorizationResults = [] + }); + } - return false; + var subordinateResult = + subordinateResults[roleAssignment.ScopeResourcePath!.MainResourceId!]; + + if (authorizationRequest.IncludeRoles + && !subordinateResult.Roles.Contains(roleAssignment.RoleDefinition!.DisplayName!)) + { + subordinateResult.Roles.Add(roleAssignment.RoleDefinition!.DisplayName!); + } + + if (roleAssignment.AllowedActions.Contains(authorizationRequest.Action)) + { + subordinateResult.Authorized = true; + } + } + } + + result.SubordinateResourcePathsAuthorizationResults = + subordinateResults; + } + } + + return result; } } } diff --git a/src/dotnet/Authorization/Services/AuthorizationService.cs b/src/dotnet/Authorization/Services/AuthorizationService.cs index fb9e2aac51..6546225eee 100644 --- a/src/dotnet/Authorization/Services/AuthorizationService.cs +++ b/src/dotnet/Authorization/Services/AuthorizationService.cs @@ -1,6 +1,7 @@ using FoundationaLLM.Authorization.Models.Configuration; using FoundationaLLM.Common.Authentication; using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models; using FoundationaLLM.Common.Models.Authentication; using FoundationaLLM.Common.Models.Authorization; using Microsoft.Extensions.Logging; @@ -35,9 +36,18 @@ public async Task ProcessAuthorizationRequest( string instanceId, string action, List resourcePaths, + bool expandResourceTypePaths, + bool includeRoleAssignments, UnifiedUserIdentity userIdentity) { - var defaultResults = resourcePaths.Distinct().ToDictionary(rp => rp, auth => false); + var defaultResults = resourcePaths.Distinct().ToDictionary( + rp => rp, + rp => new ResourcePathAuthorizationResult + { + ResourceName = string.Empty, + ResourcePath = rp, + Authorized = false + }); try { @@ -45,8 +55,14 @@ public async Task ProcessAuthorizationRequest( { Action = action, ResourcePaths = resourcePaths, - PrincipalId = userIdentity.UserId, - SecurityGroupIds = userIdentity.GroupIds + ExpandResourceTypePaths = expandResourceTypePaths, + IncludeRoles = includeRoleAssignments, + UserContext = new UserAuthorizationContext + { + SecurityPrincipalId = userIdentity.UserId!, + UserPrincipalName = userIdentity.UPN!, + SecurityGroupIds = userIdentity.GroupIds + } }; var httpClient = await CreateHttpClient(); @@ -69,8 +85,9 @@ public async Task ProcessAuthorizationRequest( } } + /// - public async Task ProcessRoleAssignmentRequest( + public async Task CreateRoleAssignment( string instanceId, RoleAssignmentRequest roleAssignmentRequest, UnifiedUserIdentity userIdentity) @@ -85,58 +102,26 @@ public async Task ProcessRoleAssignmentRequest( if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonSerializer.Deserialize(responseContent); + var result = JsonSerializer.Deserialize(responseContent); if (result == null) - return new RoleAssignmentResult() { Success = false }; + return new RoleAssignmentOperationResult() { Success = false }; return result; } _logger.LogError("The call to the Authorization API returned an error: {StatusCode} - {ReasonPhrase}.", response.StatusCode, response.ReasonPhrase); - return new RoleAssignmentResult() { Success = false }; - } - catch (Exception ex) - { - _logger.LogError(ex, "There was an error calling the Authorization API"); - return new RoleAssignmentResult() { Success = false }; - } - } - - /// - public async Task> ProcessRoleAssignmentsWithActionsRequest( - string instanceId, - RoleAssignmentsWithActionsRequest request, - UnifiedUserIdentity userIdentity) - { - var defaultResults = request.Scopes.Distinct().ToDictionary(scp => scp, res => new RoleAssignmentsWithActionsResult() { Actions = [], Roles = [] }); - - try - { - var httpClient = await CreateHttpClient(); - var response = await httpClient.PostAsync( - $"/instances/{instanceId}/roleassignments/querywithactions", - JsonContent.Create(request)); - - if (response.IsSuccessStatusCode) - { - var responseContent = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize>(responseContent)!; - } - - _logger.LogError("The call to the Authorization API returned an error: {StatusCode} - {ReasonPhrase}.", response.StatusCode, response.ReasonPhrase); - return defaultResults; + return new RoleAssignmentOperationResult() { Success = false }; } catch (Exception ex) { _logger.LogError(ex, "There was an error calling the Authorization API"); - return defaultResults; + return new RoleAssignmentOperationResult() { Success = false }; } } - /// - public async Task> GetRoleAssignments( + public async Task> GetRoleAssignments( string instanceId, RoleAssignmentQueryParameters queryParameters, UnifiedUserIdentity userIdentity) @@ -151,7 +136,7 @@ public async Task> GetRoleAssignments( if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize>(responseContent)!; + return JsonSerializer.Deserialize>(responseContent)!; } _logger.LogError("The call to the Authorization API returned an error: {StatusCode} - {ReasonPhrase}.", response.StatusCode, response.ReasonPhrase); @@ -165,7 +150,7 @@ public async Task> GetRoleAssignments( } /// - public async Task RevokeRoleAssignment( + public async Task DeleteRoleAssignment( string instanceId, string roleAssignment, UnifiedUserIdentity userIdentity) @@ -179,21 +164,21 @@ public async Task RevokeRoleAssignment( if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonSerializer.Deserialize(responseContent); + var result = JsonSerializer.Deserialize(responseContent); if (result == null) - return new RoleAssignmentResult() { Success = false }; + return new RoleAssignmentOperationResult() { Success = false }; return result; } _logger.LogError("The call to the Authorization API returned an error: {StatusCode} - {ReasonPhrase}.", response.StatusCode, response.ReasonPhrase); - return new RoleAssignmentResult() { Success = false }; + return new RoleAssignmentOperationResult() { Success = false }; } catch (Exception ex) { _logger.LogError(ex, "There was an error calling the Authorization API"); - return new RoleAssignmentResult() { Success = false }; + return new RoleAssignmentOperationResult() { Success = false }; } } diff --git a/src/dotnet/Authorization/Services/DependencyInjection.cs b/src/dotnet/Authorization/Services/DependencyInjection.cs index 78ddef0c35..67df8eea7b 100644 --- a/src/dotnet/Authorization/Services/DependencyInjection.cs +++ b/src/dotnet/Authorization/Services/DependencyInjection.cs @@ -23,7 +23,7 @@ namespace FoundationaLLM public static partial class DependencyInjection { /// - /// Add the Authorization Core service to the dependency injection container (used by the Authorization API). + /// Adds the Authorization Core service to the dependency injection container (used by the Authorization API). /// /// The host application builder. public static void AddAuthorizationCore(this IHostApplicationBuilder builder) @@ -59,7 +59,7 @@ public static void AddAuthorizationCore(this IHostApplicationBuilder builder) } /// - /// Add the authorization service to the dependency injection container (used by all resource providers). + /// Adds the authorization service to the dependency injection container (used by all resource providers). /// /// public static void AddAuthorizationService(this IHostApplicationBuilder builder) @@ -70,7 +70,7 @@ public static void AddAuthorizationService(this IHostApplicationBuilder builder) } /// - /// Add the authorization service to the dependency injection container (used by all resource providers). + /// Adds the authorization service to the dependency injection container (used by all resource providers). /// /// The dependency injection container service collection. /// The application configuration manager. diff --git a/src/dotnet/Authorization/Services/NullAuthorizationService.cs b/src/dotnet/Authorization/Services/NullAuthorizationService.cs index 0c26f1881b..c37ae1e70b 100644 --- a/src/dotnet/Authorization/Services/NullAuthorizationService.cs +++ b/src/dotnet/Authorization/Services/NullAuthorizationService.cs @@ -1,4 +1,5 @@ using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models; using FoundationaLLM.Common.Models.Authentication; using FoundationaLLM.Common.Models.Authorization; @@ -14,38 +15,35 @@ public async Task ProcessAuthorizationRequest( string instanceId, string action, List resourcePaths, + bool expandResourceTypePaths, + bool includeRoleAssignments, UnifiedUserIdentity userIdentity) { - var defaultResults = resourcePaths.Distinct().ToDictionary(rp => rp, auth => true); + var defaultResults = resourcePaths.Distinct().ToDictionary( + rp => rp, + rp => new ResourcePathAuthorizationResult + { + ResourceName = string.Empty, + ResourcePath = rp, + Authorized = true + }); await Task.CompletedTask; return new ActionAuthorizationResult { AuthorizationResults = defaultResults }; } /// - public async Task ProcessRoleAssignmentRequest( + public async Task CreateRoleAssignment( string instanceId, RoleAssignmentRequest roleAssignmentRequest, UnifiedUserIdentity userIdentity) { await Task.CompletedTask; - return new RoleAssignmentResult { Success = true }; + return new RoleAssignmentOperationResult { Success = true }; } /// - public async Task> ProcessRoleAssignmentsWithActionsRequest( - string instanceId, - RoleAssignmentsWithActionsRequest request, - UnifiedUserIdentity userIdentity) - { - var defaultResults = request.Scopes.Distinct().ToDictionary(scp => scp, res => new RoleAssignmentsWithActionsResult() { Actions = [], Roles = [] }); - - await Task.CompletedTask; - return defaultResults; - } - - /// - public async Task> GetRoleAssignments( + public async Task> GetRoleAssignments( string instanceId, RoleAssignmentQueryParameters queryParameters, UnifiedUserIdentity userIdentity) @@ -54,13 +52,13 @@ public async Task> GetRoleAssignments( return []; } - public async Task RevokeRoleAssignment( + public async Task DeleteRoleAssignment( string instanceId, string roleAssignment, UnifiedUserIdentity userIdentity) { await Task.CompletedTask; - return new RoleAssignmentResult { Success = true }; + return new RoleAssignmentOperationResult { Success = true }; } } } diff --git a/src/dotnet/Authorization/Validation/ActionAuthorizationRequestValidator.cs b/src/dotnet/Authorization/Validation/ActionAuthorizationRequestValidator.cs index ec9b34509e..bac4909800 100644 --- a/src/dotnet/Authorization/Validation/ActionAuthorizationRequestValidator.cs +++ b/src/dotnet/Authorization/Validation/ActionAuthorizationRequestValidator.cs @@ -22,24 +22,33 @@ public ActionAuthorizationRequestValidator() .Must(x => AuthorizableActions.Actions.ContainsKey(x)) .WithMessage("The action must be a valid action."); - RuleFor(x => x.PrincipalId) + RuleFor(x => x.UserContext) + .NotNull() + .WithMessage("The user context must be a valid object."); + + RuleFor(x => x.UserContext.SecurityPrincipalId) .NotNull() .NotEmpty() - .WithMessage("The principal identifier must be a valid string.") + .WithMessage("The security principal identifier provided in the user context must be a valid string.") .Must(x => Guid.TryParse(x, out _)) - .WithMessage("The principal identifier must be a valid GUID."); + .WithMessage("The security principal identifier provided in the user context must be a valid GUID."); + + RuleFor(x => x.UserContext.UserPrincipalName) + .NotNull() + .NotEmpty() + .WithMessage("The user principal name provided in the user context must be a valid string."); - RuleForEach(x => x.SecurityGroupIds) + RuleForEach(x => x.UserContext.SecurityGroupIds) .NotNull() .NotEmpty() - .WithMessage("The security group identifier must be a valid string.") + .WithMessage("Every security group identifier provided in the user context must be a valid string.") .Must(x => Guid.TryParse(x, out _)) - .WithMessage("The security group identifier must be a valid GUID."); + .WithMessage("Every security group identifier provided in the user context must be a valid GUID."); RuleForEach(x => x.ResourcePaths) .NotNull() .NotEmpty() - .WithMessage("The resource path must be a valid string."); + .WithMessage("Each resource path must be a valid string."); } } } diff --git a/src/dotnet/AuthorizationAPI/Controllers/RoleAssignmentsController.cs b/src/dotnet/AuthorizationAPI/Controllers/RoleAssignmentsController.cs index b938bc25bf..7676cf3d41 100644 --- a/src/dotnet/AuthorizationAPI/Controllers/RoleAssignmentsController.cs +++ b/src/dotnet/AuthorizationAPI/Controllers/RoleAssignmentsController.cs @@ -21,16 +21,6 @@ public class RoleAssignmentsController( #region IAuthorizationCore - /// - /// Returns a list of role names and a list of allowed actions for the specified scope. - /// - /// The FoundationaLLM instance identifier. - /// The get roles with actions request. - /// The get roles and actions result. - [HttpPost("querywithactions")] - public IActionResult ProcessRoleAssignmentsWithActionsRequest(string instanceId, [FromBody] RoleAssignmentsWithActionsRequest request) => - new OkObjectResult(_authorizationCore.ProcessRoleAssignmentsWithActionsRequest(instanceId, request)); - /// /// Returns a list of role assignments for the specified instance. /// @@ -58,7 +48,7 @@ public async Task AssignRole(string instanceId, RoleAssignmentReq /// The role assignment result. [HttpDelete("{*roleAssignment}")] public async Task RevokeRoleAssignment(string instanceId, string roleAssignment) => - new OkObjectResult(await _authorizationCore.RevokeRoleAssignment(instanceId, roleAssignment)); + new OkObjectResult(await _authorizationCore.DeleteRoleAssignment(instanceId, roleAssignment)); #endregion } diff --git a/src/dotnet/AzureOpenAI/ResourceProviders/AzureOpenAIResourceProviderService.cs b/src/dotnet/AzureOpenAI/ResourceProviders/AzureOpenAIResourceProviderService.cs index 030faf928a..80ef2ea548 100644 --- a/src/dotnet/AzureOpenAI/ResourceProviders/AzureOpenAIResourceProviderService.cs +++ b/src/dotnet/AzureOpenAI/ResourceProviders/AzureOpenAIResourceProviderService.cs @@ -11,6 +11,7 @@ using FoundationaLLM.Common.Extensions; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models.Authorization; using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.AzureOpenAI; @@ -50,7 +51,7 @@ public class AzureOpenAIResourceProviderService( serviceProvider, logger, eventNamespacesToSubscribe: null, - useInternalStore: true) + useInternalReferencesStore: true) { private readonly SemaphoreSlim _localLock = new(1, 1); @@ -67,105 +68,59 @@ protected override async Task InitializeInternal() => #region Resource provider support for Management API /// - protected override async Task GetResourcesAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + protected override async Task GetResourcesAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity, + ResourceProviderLoadOptions? options = null) => + resourcePath.MainResourceTypeName switch { - AzureOpenAIResourceTypeNames.AssistantUserContexts => await LoadResources(resourcePath.ResourceTypeInstances[0]), - AzureOpenAIResourceTypeNames.FileUserContexts => await LoadResources(resourcePath.ResourceTypeInstances[0]), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + AzureOpenAIResourceTypeNames.AssistantUserContexts => await LoadResources( + resourcePath.ResourceTypeInstances[0], + authorizationResult), + AzureOpenAIResourceTypeNames.FileUserContexts => await LoadResources( + resourcePath.ResourceTypeInstances[0], + authorizationResult), + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }; /// - protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + protected override async Task ExecuteActionAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + string serializedAction, + UnifiedUserIdentity userIdentity) => + resourcePath.ResourceTypeName switch { - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest), - }; - - /// -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected override async Task ExecuteActionAsync(ResourcePath resourcePath, string serializedAction, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances.Last().ResourceType switch - { - AzureOpenAIResourceTypeNames.AssistantUserContexts => resourcePath.ResourceTypeInstances.Last().Action switch + AzureOpenAIResourceTypeNames.AssistantUserContexts => resourcePath.Action switch { - ResourceProviderActions.CheckName => await CheckResourceName(serializedAction), - ResourceProviderActions.Purge => await PurgeResource(resourcePath), - _ => throw new ResourceProviderException($"The action {resourcePath.ResourceTypeInstances.Last().Action} is not supported by the {_name} resource provider.", + ResourceProviderActions.CheckName => await CheckResourceName( + JsonSerializer.Deserialize(serializedAction)!), + ResourceProviderActions.Purge => await PurgeResource(resourcePath), + _ => throw new ResourceProviderException( + $"The action {resourcePath.Action} is not supported for the resource type {AzureOpenAIResourceTypeNames.AssistantUserContexts} by the {_name} resource provider.", StatusCodes.Status400BadRequest) }, _ => throw new ResourceProviderException() }; -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - - #region Helpers for ExecuteActionAsync - - private async Task CheckResourceName(string serializedAction) - { - var resourceName = JsonSerializer.Deserialize(serializedAction); - var resourceReference = await _resourceReferenceStore!.GetResourceReference(resourceName!.Name); - - return resourceReference != null - ? new ResourceNameCheckResult - { - Name = resourceName!.Name, - Type = resourceName.Type, - Status = NameCheckResultType.Denied, - Message = "A resource with the specified name already exists or was previously deleted and not purged." - } - : new ResourceNameCheckResult - { - Name = resourceName!.Name, - Type = resourceName.Type, - Status = NameCheckResultType.Allowed - }; - } - - private async Task PurgeResource(ResourcePath resourcePath) - { - await Task.CompletedTask; - throw new NotImplementedException("The Azure OpenAI resource cleanup is not implemented."); - } - - #endregion - - /// - protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) - { - switch (resourcePath.ResourceTypeInstances.Last().ResourceType) - { - case AzureOpenAIResourceTypeNames.AssistantUserContexts: - await DeleteAssistantUserContext(resourcePath.ResourceTypeInstances); - break; - default: - throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances.Last().ResourceType} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest); - }; - } - - #region Helpers for DeleteResourceAsync - - private async Task DeleteAssistantUserContext(List instances) => - throw new NotImplementedException("The Azure OpenAI resource deletion is not implemented."); - - #endregion #endregion #region Resource provider strongly typed operations /// - protected override async Task GetResourceInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderOptions? options = null) => - resourcePath.ResourceTypeInstances.Last().ResourceType switch + protected override async Task GetResourceAsyncInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) => + resourcePath.ResourceTypeName switch { + AzureOpenAIResourceTypeNames.AssistantUserContexts => (await LoadResource( + resourcePath.MainResourceId!))!, AzureOpenAIResourceTypeNames.FilesContent => ((await LoadFileContent( - resourcePath.ResourceTypeInstances[0].ResourceId!, - resourcePath.ResourceTypeInstances[1].ResourceId!)) as T)!, - AzureOpenAIResourceTypeNames.FileUserContexts => ((await LoadFileUserContext(resourcePath.ResourceTypeInstances[0].ResourceId!)) as T)!, + resourcePath.MainResourceId!, + resourcePath.ResourceId!)) as T)!, + AzureOpenAIResourceTypeNames.FileUserContexts => ((await LoadFileUserContext(resourcePath.MainResourceId!)) as T)!, _ => throw new ResourceProviderException( - $"The {resourcePath.MainResourceType} resource type is not supported by the {_name} resource provider.") + $"The {resourcePath.MainResourceTypeName} resource type is not supported by the {_name} resource provider.") }; /// @@ -181,15 +136,25 @@ protected override async Task UpsertResourceAsyncInternal(R private async Task LoadFileUserContext(string fileUserContextName) { - var resourceReference = await _resourceReferenceStore!.GetResourceReference(fileUserContextName) - ?? throw new ResourceProviderException( - $"The resource {fileUserContextName} was not found.", - StatusCodes.Status404NotFound); - - return await LoadResource(resourceReference) - ?? throw new ResourceProviderException( - $"The resource {fileUserContextName} has a valid resource reference but cannot be loaded from the storage. This might indicate a missing resource file.", - StatusCodes.Status500InternalServerError); + try + { + await _localLock.WaitAsync(); + + var resourceReference = await _resourceReferenceStore!.GetResourceReference(fileUserContextName) + ?? throw new ResourceProviderException( + $"The resource {fileUserContextName} was not found.", + StatusCodes.Status404NotFound); + + return await LoadResource(resourceReference) + ?? throw new ResourceProviderException( + $"The resource {fileUserContextName} has a valid resource reference but cannot be loaded from the storage. This might indicate a missing resource file.", + StatusCodes.Status500InternalServerError); + + } + finally + { + _localLock.Release(); + } } private async Task LoadFileContent(string fileUserContextName, string openAIFileId) @@ -353,6 +318,7 @@ await CreateResources( return new AssistantUserContextUpsertResult { ObjectId = assistantUserContext.ObjectId, + ResourceExists = false, NewOpenAIAssistantId = newOpenAIAssistantId, NewOpenAIAssistantThreadId = newOpenAIAssistantThreadId, NewOpenAIAssistantVectorStoreId = newOpenAIAssistantVectorStoreId @@ -413,6 +379,7 @@ await CreateResources( return new AssistantUserContextUpsertResult { ObjectId = existingAssistantUserContext.ObjectId, + ResourceExists = true, NewOpenAIAssistantId = newOpenAIAssistantId, NewOpenAIAssistantThreadId = newOpenAIAssistantThreadId, NewOpenAIAssistantVectorStoreId = newOpenAIAssistantVectorStoreId @@ -560,6 +527,7 @@ await _serviceProvider.GetRequiredService() return new FileUserContextUpsertResult { ObjectId = existingFileUserContext.ObjectId, + ResourceExists = false, NewOpenAIFileId = newOpenAIFileId! }; } @@ -631,6 +599,7 @@ await _serviceProvider.GetRequiredService() return new FileUserContextUpsertResult { ObjectId = existingFileUserContext.ObjectId, + ResourceExists = true, NewOpenAIFileId = newOpenAIFileId! }; } diff --git a/src/dotnet/AzureOpenAI/ResourceProviders/DependencyInjection.cs b/src/dotnet/AzureOpenAI/ResourceProviders/DependencyInjection.cs index bd62c50eb9..5f7374035b 100644 --- a/src/dotnet/AzureOpenAI/ResourceProviders/DependencyInjection.cs +++ b/src/dotnet/AzureOpenAI/ResourceProviders/DependencyInjection.cs @@ -16,32 +16,17 @@ namespace FoundationaLLM public static partial class DependencyInjection { /// - /// Register the handler as a hosted service, passing the step name to the handler ctor + /// Registers the FoundationaLLM.AzureOpenAI resource provider as a singleton service. /// - /// The application builder. + /// The application builder managing the dependency injection container. /// /// Requires an service to be also registered with the dependency injection container. /// - public static void AddAzureOpenAIResourceProvider(this IHostApplicationBuilder builder) - { - builder.AddAzureOpenAIResourceProviderStorage(); - - builder.Services.AddSingleton(sp => - new AzureOpenAIResourceProviderService( - sp.GetRequiredService>(), - sp.GetRequiredService(), - sp.GetRequiredService>() - .Single(s => s.InstanceName == DependencyInjectionKeys.FoundationaLLM_ResourceProviders_AzureOpenAI), - sp.GetRequiredService(), - sp.GetRequiredService(), - sp, - sp.GetRequiredService>())); - - builder.Services.ActivateSingleton(); - } + public static void AddAzureOpenAIResourceProvider(this IHostApplicationBuilder builder) => + builder.Services.AddAzureOpenAIResourceProvider(builder.Configuration); /// - /// Register the handler as a hosted service, passing the step name to the handler ctor + /// Registers the FoundationaLLM.AzureOpenAI resource provider as a singleton service. /// /// The dependency injection container service collection. /// The configuration manager. diff --git a/src/dotnet/Common/Common.csproj b/src/dotnet/Common/Common.csproj index d56ad066a6..c875a3fb74 100644 --- a/src/dotnet/Common/Common.csproj +++ b/src/dotnet/Common/Common.csproj @@ -58,6 +58,7 @@ + @@ -69,6 +70,7 @@ + diff --git a/src/dotnet/Common/Constants/Authorization/AuthorizableOperations.cs b/src/dotnet/Common/Constants/Authorization/AuthorizableOperations.cs new file mode 100644 index 0000000000..346f06cb81 --- /dev/null +++ b/src/dotnet/Common/Constants/Authorization/AuthorizableOperations.cs @@ -0,0 +1,23 @@ +namespace FoundationaLLM.Common.Constants.Authorization +{ + /// + /// Provides the names of the core authorizable operations. + /// + public static class AuthorizableOperations + { + /// + /// Read resources. + /// + public const string Read = "read"; + + /// + /// Create or update resources. + /// + public const string Write = "write"; + + /// + /// Delete or purge resources. + /// + public const string Delete = "delete"; + } +} diff --git a/src/dotnet/Core/Models/SessionTypes.cs b/src/dotnet/Common/Constants/Chat/ConversationTypes.cs similarity index 82% rename from src/dotnet/Core/Models/SessionTypes.cs rename to src/dotnet/Common/Constants/Chat/ConversationTypes.cs index bd4b2d5e33..ca88e5ffdc 100644 --- a/src/dotnet/Core/Models/SessionTypes.cs +++ b/src/dotnet/Common/Constants/Chat/ConversationTypes.cs @@ -1,9 +1,9 @@ -namespace FoundationaLLM.Core.Models +namespace FoundationaLLM.Common.Constants.Chat { /// /// Constants for chat session types. /// - public static class SessionTypes + public static class ConversationTypes { /// /// Named session with matching type for the KioskSession. diff --git a/src/dotnet/Common/Constants/EventSetEventNamespaces.cs b/src/dotnet/Common/Constants/EventSetEventNamespaces.cs index e5ac1e2d4d..358fae6ba2 100644 --- a/src/dotnet/Common/Constants/EventSetEventNamespaces.cs +++ b/src/dotnet/Common/Constants/EventSetEventNamespaces.cs @@ -39,5 +39,10 @@ public static class EventSetEventNamespaces /// The namespace name for events concerning the FoundationaLLM.AzureOpenAI resource provider. /// public const string FoundationaLLM_ResourceProvider_AzureOpenAI = "ResourceProvider.FoundationaLLM.AzureOpenAI"; + + /// + /// The namespace name for events concerning the FoundationaLLM.Conversation resource provider. + /// + public const string FoundationaLLM_ResourceProvider_Conversation = "ResourceProvider.FoundationaLLM.Conversation"; } } diff --git a/src/dotnet/Common/Constants/ResourceProviders/AIModelResourceProviderMetadata.cs b/src/dotnet/Common/Constants/ResourceProviders/AIModelResourceProviderMetadata.cs index 228ff54529..5e7c6b0105 100644 --- a/src/dotnet/Common/Constants/ResourceProviders/AIModelResourceProviderMetadata.cs +++ b/src/dotnet/Common/Constants/ResourceProviders/AIModelResourceProviderMetadata.cs @@ -1,4 +1,5 @@ -using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Constants.Authorization; +using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.AIModel; namespace FoundationaLLM.Common.Constants.ResourceProviders @@ -16,19 +17,20 @@ public static class AIModelResourceProviderMetadata { AIModelResourceTypeNames.AIModels, new ResourceTypeDescriptor( - AIModelResourceTypeNames.AIModels) + AIModelResourceTypeNames.AIModels, + typeof(AIModelBase)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(AIModelBase)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(AIModelBase)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ], Actions = [ new ResourceTypeAction(ResourceProviderActions.CheckName, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) ]), new ResourceTypeAction(ResourceProviderActions.Purge, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(ResourceProviderActionResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Delete, [], [], [typeof(ResourceProviderActionResult)]) ]) ] } diff --git a/src/dotnet/Common/Constants/ResourceProviders/AgentResourceProviderMetadata.cs b/src/dotnet/Common/Constants/ResourceProviders/AgentResourceProviderMetadata.cs index 3897ba4cb2..1333106674 100644 --- a/src/dotnet/Common/Constants/ResourceProviders/AgentResourceProviderMetadata.cs +++ b/src/dotnet/Common/Constants/ResourceProviders/AgentResourceProviderMetadata.cs @@ -1,4 +1,5 @@ -using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Constants.Authorization; +using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Agent; namespace FoundationaLLM.Common.Constants.ResourceProviders @@ -16,19 +17,20 @@ public static class AgentResourceProviderMetadata { AgentResourceTypeNames.Agents, new ResourceTypeDescriptor( - AgentResourceTypeNames.Agents) + AgentResourceTypeNames.Agents, + typeof(AgentBase)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(AgentBase)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(AgentBase)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ], Actions = [ new ResourceTypeAction(ResourceProviderActions.CheckName, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) ]), new ResourceTypeAction(ResourceProviderActions.Purge, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(ResourceProviderActionResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Delete, [], [], [typeof(ResourceProviderActionResult)]) ]) ] } diff --git a/src/dotnet/Common/Constants/ResourceProviders/AttachmentResourceProviderMetadata.cs b/src/dotnet/Common/Constants/ResourceProviders/AttachmentResourceProviderMetadata.cs index a35ff8a116..00f1256a15 100644 --- a/src/dotnet/Common/Constants/ResourceProviders/AttachmentResourceProviderMetadata.cs +++ b/src/dotnet/Common/Constants/ResourceProviders/AttachmentResourceProviderMetadata.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Constants.Authorization; using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Attachment; @@ -17,16 +17,17 @@ public static class AttachmentResourceProviderMetadata { AttachmentResourceTypeNames.Attachments, new ResourceTypeDescriptor( - AttachmentResourceTypeNames.Attachments) + AttachmentResourceTypeNames.Attachments, + typeof(AttachmentFile)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(AttachmentFile)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(AttachmentFile)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ], Actions = [ new ResourceTypeAction(ResourceProviderActions.Filter, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceFilter)], [typeof(AttachmentDetail)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceFilter)], [typeof(AttachmentFile)]) ]) ] } diff --git a/src/dotnet/Common/Constants/ResourceProviders/AuthorizationResourceProviderMetadata.cs b/src/dotnet/Common/Constants/ResourceProviders/AuthorizationResourceProviderMetadata.cs index 52221723f2..87ea8d623a 100644 --- a/src/dotnet/Common/Constants/ResourceProviders/AuthorizationResourceProviderMetadata.cs +++ b/src/dotnet/Common/Constants/ResourceProviders/AuthorizationResourceProviderMetadata.cs @@ -1,4 +1,5 @@ -using FoundationaLLM.Common.Models; +using FoundationaLLM.Common.Constants.Authorization; +using FoundationaLLM.Common.Models; using FoundationaLLM.Common.Models.Authorization; using FoundationaLLM.Common.Models.ResourceProviders; @@ -17,15 +18,16 @@ public class AuthorizationResourceProviderMetadata { AuthorizationResourceTypeNames.RoleAssignments, new ResourceTypeDescriptor( - AuthorizationResourceTypeNames.RoleAssignments) + AuthorizationResourceTypeNames.RoleAssignments, + typeof(RoleAssignment)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(RoleAssignment)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(RoleAssignment)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []) ], Actions = [ new ResourceTypeAction(ResourceProviderActions.Filter, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(RoleAssignmentQueryParameters)], [typeof(ResourceProviderGetResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(RoleAssignmentQueryParameters)], [typeof(ResourceProviderGetResult)]) ]) ] } @@ -33,10 +35,11 @@ public class AuthorizationResourceProviderMetadata { AuthorizationResourceTypeNames.RoleDefinitions, new ResourceTypeDescriptor( - AuthorizationResourceTypeNames.RoleDefinitions) + AuthorizationResourceTypeNames.RoleDefinitions, + typeof(RoleDefinition)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(RoleDefinition)]) + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(RoleDefinition)]) ], Actions = [] } diff --git a/src/dotnet/Common/Constants/ResourceProviders/AzureOpenAIResourceProviderMetadata.cs b/src/dotnet/Common/Constants/ResourceProviders/AzureOpenAIResourceProviderMetadata.cs index ce7db5ecf8..a854f89155 100644 --- a/src/dotnet/Common/Constants/ResourceProviders/AzureOpenAIResourceProviderMetadata.cs +++ b/src/dotnet/Common/Constants/ResourceProviders/AzureOpenAIResourceProviderMetadata.cs @@ -1,4 +1,5 @@ -using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Constants.Authorization; +using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.AzureOpenAI; namespace FoundationaLLM.Common.Constants.ResourceProviders @@ -16,19 +17,20 @@ public static class AzureOpenAIResourceProviderMetadata { AzureOpenAIResourceTypeNames.AssistantUserContexts, new ResourceTypeDescriptor( - AzureOpenAIResourceTypeNames.AssistantUserContexts) + AzureOpenAIResourceTypeNames.AssistantUserContexts, + typeof(AssistantUserContext)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(AssistantUserContext)], [typeof(AssistantUserContextUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(AssistantUserContext)], [typeof(AssistantUserContextUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ], Actions = [ new ResourceTypeAction(ResourceProviderActions.CheckName, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) ]), new ResourceTypeAction(ResourceProviderActions.Purge, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(ResourceProviderActionResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Delete, [], [], [typeof(ResourceProviderActionResult)]) ]) ] } @@ -36,28 +38,31 @@ public static class AzureOpenAIResourceProviderMetadata { AzureOpenAIResourceTypeNames.FileUserContexts, new ResourceTypeDescriptor( - AzureOpenAIResourceTypeNames.FileUserContexts) + AzureOpenAIResourceTypeNames.FileUserContexts, + typeof(FileUserContext)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(FileUserContext)], [typeof(FileUserContextUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []) + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(FileUserContext)], [typeof(FileUserContextUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []) ], SubTypes = new() { { AzureOpenAIResourceTypeNames.FilesContent, new ResourceTypeDescriptor ( - AzureOpenAIResourceTypeNames.FilesContent) + AzureOpenAIResourceTypeNames.FilesContent, + typeof(FileContent)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]) ] } } } } } - }; + }; + } } diff --git a/src/dotnet/Common/Constants/ResourceProviders/ConfigurationResourceProviderMetadata.cs b/src/dotnet/Common/Constants/ResourceProviders/ConfigurationResourceProviderMetadata.cs index 118582db4e..1907f25a5e 100644 --- a/src/dotnet/Common/Constants/ResourceProviders/ConfigurationResourceProviderMetadata.cs +++ b/src/dotnet/Common/Constants/ResourceProviders/ConfigurationResourceProviderMetadata.cs @@ -1,4 +1,5 @@ -using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Constants.Authorization; +using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Agent; using FoundationaLLM.Common.Models.ResourceProviders.Configuration; @@ -17,16 +18,17 @@ public static class ConfigurationResourceProviderMetadata { ConfigurationResourceTypeNames.AppConfigurations, new ResourceTypeDescriptor( - ConfigurationResourceTypeNames.AppConfigurations) + ConfigurationResourceTypeNames.AppConfigurations, + typeof(AppConfigurationKeyBase)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(AgentBase)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(AgentBase)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ], Actions = [ new ResourceTypeAction(ResourceProviderActions.CheckName, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) ]) ] } @@ -35,12 +37,13 @@ public static class ConfigurationResourceProviderMetadata { ConfigurationResourceTypeNames.APIEndpointConfigurations, new ResourceTypeDescriptor( - ConfigurationResourceTypeNames.APIEndpointConfigurations) + ConfigurationResourceTypeNames.APIEndpointConfigurations, + typeof(APIEndpointConfiguration)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(APIEndpointConfiguration)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(APIEndpointConfiguration)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ] } } diff --git a/src/dotnet/Common/Constants/ResourceProviders/ConversationResourceProviderMetadata.cs b/src/dotnet/Common/Constants/ResourceProviders/ConversationResourceProviderMetadata.cs new file mode 100644 index 0000000000..19d5803e7c --- /dev/null +++ b/src/dotnet/Common/Constants/ResourceProviders/ConversationResourceProviderMetadata.cs @@ -0,0 +1,32 @@ +using FoundationaLLM.Common.Constants.Authorization; +using FoundationaLLM.Common.Models.Conversation; +using FoundationaLLM.Common.Models.ResourceProviders; + +namespace FoundationaLLM.Common.Constants.ResourceProviders +{ + /// + /// Provides metadata for the FoundationaLLM.Conversation resource provider. + /// + public static class ConversationResourceProviderMetadata + { + /// + /// The metadata describing the resource types allowed by the resource provider. + /// + public static Dictionary AllowedResourceTypes => new() + { + { + ConversationResourceTypeNames.Conversations, + new ResourceTypeDescriptor( + ConversationResourceTypeNames.Conversations, + typeof(Conversation)) + { + AllowedTypes = [ + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(Conversation)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), + ] + } + } + }; + } +} diff --git a/src/dotnet/Common/Constants/ResourceProviders/ConversationResourceTypeNames.cs b/src/dotnet/Common/Constants/ResourceProviders/ConversationResourceTypeNames.cs new file mode 100644 index 0000000000..3b961988b3 --- /dev/null +++ b/src/dotnet/Common/Constants/ResourceProviders/ConversationResourceTypeNames.cs @@ -0,0 +1,13 @@ +namespace FoundationaLLM.Common.Constants.ResourceProviders +{ + /// + /// Contains constants of the names of the resource types managed by the FoundationaLLM.Conversation resource provider. + /// + public class ConversationResourceTypeNames + { + /// + /// OpenAI assistant user contexts. + /// + public const string Conversations = "conversations"; + } +} diff --git a/src/dotnet/Common/Constants/ResourceProviders/DataSourceResourceProviderMetadata.cs b/src/dotnet/Common/Constants/ResourceProviders/DataSourceResourceProviderMetadata.cs index 82c92e6d05..e81124d44e 100644 --- a/src/dotnet/Common/Constants/ResourceProviders/DataSourceResourceProviderMetadata.cs +++ b/src/dotnet/Common/Constants/ResourceProviders/DataSourceResourceProviderMetadata.cs @@ -1,4 +1,5 @@ -using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Constants.Authorization; +using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.DataSource; namespace FoundationaLLM.Common.Constants.ResourceProviders @@ -16,22 +17,23 @@ public static class DataSourceResourceProviderMetadata { DataSourceResourceTypeNames.DataSources, new ResourceTypeDescriptor( - DataSourceResourceTypeNames.DataSources) + DataSourceResourceTypeNames.DataSources, + typeof(DataSourceBase)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(DataSourceBase)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(DataSourceBase)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ], Actions = [ new ResourceTypeAction(ResourceProviderActions.CheckName, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) ]), new ResourceTypeAction(ResourceProviderActions.Filter, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceFilter)], [typeof(DataSourceBase)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceFilter)], [typeof(DataSourceBase)]) ]), new ResourceTypeAction(ResourceProviderActions.Purge, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(ResourceProviderActionResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Delete, [], [], [typeof(ResourceProviderActionResult)]) ]) ] } diff --git a/src/dotnet/Common/Constants/ResourceProviders/PromptResourceProviderMetadata.cs b/src/dotnet/Common/Constants/ResourceProviders/PromptResourceProviderMetadata.cs index ae49365bda..fe3725d9d0 100644 --- a/src/dotnet/Common/Constants/ResourceProviders/PromptResourceProviderMetadata.cs +++ b/src/dotnet/Common/Constants/ResourceProviders/PromptResourceProviderMetadata.cs @@ -1,4 +1,5 @@ -using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Constants.Authorization; +using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Prompt; namespace FoundationaLLM.Common.Constants.ResourceProviders @@ -16,19 +17,20 @@ public static class PromptResourceProviderMetadata { PromptResourceTypeNames.Prompts, new ResourceTypeDescriptor( - PromptResourceTypeNames.Prompts) + PromptResourceTypeNames.Prompts, + typeof(PromptBase)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(MultipartPrompt)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(PromptBase)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ], Actions = [ - new ResourceTypeAction("checkname", false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) + new ResourceTypeAction(ResourceProviderActions.CheckName, false, true, [ + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) ]), new ResourceTypeAction(ResourceProviderActions.Purge, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(ResourceProviderActionResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Delete, [], [], [typeof(ResourceProviderActionResult)]) ]) ] } diff --git a/src/dotnet/Common/Constants/ResourceProviders/ResourceProviderNames.cs b/src/dotnet/Common/Constants/ResourceProviders/ResourceProviderNames.cs index 00b212d201..9591d31230 100644 --- a/src/dotnet/Common/Constants/ResourceProviders/ResourceProviderNames.cs +++ b/src/dotnet/Common/Constants/ResourceProviders/ResourceProviderNames.cs @@ -52,6 +52,11 @@ public static class ResourceProviderNames /// public const string FoundationaLLM_AzureOpenAI = "FoundationaLLM.AzureOpenAI"; + /// + /// The name of the FoundationaLLM.Conversation resource provider. + /// + public const string FoundationaLLM_Conversation = "FoundationaLLM.Conversation"; + /// /// Contains all the resource provider names. /// @@ -64,7 +69,8 @@ public static class ResourceProviderNames FoundationaLLM_Attachment, FoundationaLLM_Authorization, FoundationaLLM_AIModel, - FoundationaLLM_AzureOpenAI + FoundationaLLM_AzureOpenAI, + FoundationaLLM_Conversation ]; } } diff --git a/src/dotnet/Common/Constants/ResourceProviders/VectorizationResourceProviderMetadata.cs b/src/dotnet/Common/Constants/ResourceProviders/VectorizationResourceProviderMetadata.cs index 6958576384..d9bef28609 100644 --- a/src/dotnet/Common/Constants/ResourceProviders/VectorizationResourceProviderMetadata.cs +++ b/src/dotnet/Common/Constants/ResourceProviders/VectorizationResourceProviderMetadata.cs @@ -1,4 +1,5 @@ -using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Constants.Authorization; +using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Vectorization; using FoundationaLLM.Common.Models.Vectorization; @@ -17,22 +18,23 @@ public static class VectorizationResourceProviderMetadata { VectorizationResourceTypeNames.VectorizationPipelines, new ResourceTypeDescriptor( - VectorizationResourceTypeNames.VectorizationPipelines) + VectorizationResourceTypeNames.VectorizationPipelines, + typeof(VectorizationPipeline)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(VectorizationPipeline)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []) + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(VectorizationPipeline)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []) ], Actions = [ new ResourceTypeAction(VectorizationResourceProviderActions.Activate, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(VectorizationResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [], [typeof(VectorizationResult)]) ]), new ResourceTypeAction(VectorizationResourceProviderActions.Deactivate, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(VectorizationResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [], [typeof(VectorizationResult)]) ]), new ResourceTypeAction(ResourceProviderActions.Purge, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(ResourceProviderActionResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Delete, [], [], [typeof(ResourceProviderActionResult)]) ]) ] } @@ -40,16 +42,17 @@ public static class VectorizationResourceProviderMetadata { VectorizationResourceTypeNames.VectorizationRequests, new ResourceTypeDescriptor( - VectorizationResourceTypeNames.VectorizationRequests) + VectorizationResourceTypeNames.VectorizationRequests, + typeof(VectorizationRequest)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(VectorizationRequest)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(VectorizationRequest)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(VectorizationRequest)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(VectorizationRequest)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ], Actions = [ new ResourceTypeAction(VectorizationResourceProviderActions.Process, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(VectorizationResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [], [typeof(VectorizationResult)]) ]) ] } @@ -57,19 +60,20 @@ public static class VectorizationResourceProviderMetadata { VectorizationResourceTypeNames.TextPartitioningProfiles, new ResourceTypeDescriptor( - VectorizationResourceTypeNames.TextPartitioningProfiles) + VectorizationResourceTypeNames.TextPartitioningProfiles, + typeof(TextPartitioningProfile)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(TextPartitioningProfile)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(TextPartitioningProfile)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ], Actions = [ new ResourceTypeAction(ResourceProviderActions.CheckName, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) ]), new ResourceTypeAction(ResourceProviderActions.Purge, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(ResourceProviderActionResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Delete, [], [], [typeof(ResourceProviderActionResult)]) ]) ] } @@ -77,19 +81,20 @@ public static class VectorizationResourceProviderMetadata { VectorizationResourceTypeNames.TextEmbeddingProfiles, new ResourceTypeDescriptor( - VectorizationResourceTypeNames.TextEmbeddingProfiles) + VectorizationResourceTypeNames.TextEmbeddingProfiles, + typeof(TextEmbeddingProfile)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(TextEmbeddingProfile)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(TextEmbeddingProfile)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ], Actions = [ new ResourceTypeAction(ResourceProviderActions.CheckName, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) ]), new ResourceTypeAction(ResourceProviderActions.Purge, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(ResourceProviderActionResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Delete, [], [], [typeof(ResourceProviderActionResult)]) ]) ] } @@ -97,22 +102,23 @@ public static class VectorizationResourceProviderMetadata { VectorizationResourceTypeNames.IndexingProfiles, new ResourceTypeDescriptor( - VectorizationResourceTypeNames.IndexingProfiles) + VectorizationResourceTypeNames.IndexingProfiles, + typeof(IndexingProfile)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], [typeof(ResourceProviderGetResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(IndexingProfile)], [typeof(ResourceProviderUpsertResult)]), - new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, [], [], []), + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], [typeof(ResourceProviderGetResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Write, [], [typeof(IndexingProfile)], [typeof(ResourceProviderUpsertResult)]), + new ResourceTypeAllowedTypes(HttpMethod.Delete.Method, AuthorizableOperations.Delete, [], [], []), ], Actions = [ new ResourceTypeAction(ResourceProviderActions.CheckName, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceName)], [typeof(ResourceNameCheckResult)]) ]), new ResourceTypeAction(ResourceProviderActions.Filter, false, true, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [typeof(ResourceFilter)], [typeof(IndexingProfile)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Read, [], [typeof(ResourceFilter)], [typeof(IndexingProfile)]) ]), new ResourceTypeAction(ResourceProviderActions.Purge, true, false, [ - new ResourceTypeAllowedTypes(HttpMethod.Post.Method, [], [], [typeof(ResourceProviderActionResult)]) + new ResourceTypeAllowedTypes(HttpMethod.Post.Method, AuthorizableOperations.Delete, [], [], [typeof(ResourceProviderActionResult)]) ]) ] } diff --git a/src/dotnet/Common/Extensions/AuthorizationServiceExtensions.cs b/src/dotnet/Common/Extensions/AuthorizationServiceExtensions.cs deleted file mode 100644 index 6a4fd21277..0000000000 --- a/src/dotnet/Common/Extensions/AuthorizationServiceExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using FoundationaLLM.Common.Interfaces; -using FoundationaLLM.Common.Models.Authentication; -using FoundationaLLM.Common.Models.Authorization; -using FoundationaLLM.Common.Models.ResourceProviders; - -namespace FoundationaLLM.Common.Extensions -{ - /// - /// Extends the interface with helper methods. - /// - public static class AuthorizationServiceExtensions - { - /// - /// Filters the list of resources based on the authorizable action. - /// - /// The object type of the resource being retrieved. - /// The service. - /// The FoundationaLLM instance identifier. - /// The providing information about the calling user identity. - /// The list of all resources. - /// The authorizable action to be checked. - /// A list of resources on which the user identity is allowed to perform the authorizable action. - public static async Task>> FilterResourcesByAuthorizableAction( - this IAuthorizationService authorizationService, - string instanceId, - UnifiedUserIdentity userIdentity, - List resources, - string authorizableAction) - where T : ResourceBase - { - var rolesWithActions = await authorizationService.ProcessRoleAssignmentsWithActionsRequest( - instanceId, - new RoleAssignmentsWithActionsRequest() - { - Scopes = resources.Select(x => x.ObjectId!).ToList(), - PrincipalId = userIdentity.UserId!, - SecurityGroupIds = userIdentity.GroupIds - }, - userIdentity); - - var results = new List>(); - - foreach (var resource in resources) - if (rolesWithActions[resource.ObjectId!].Actions.Contains(authorizableAction)) - results.Add(new ResourceProviderGetResult() - { - Resource = resource, - Actions = rolesWithActions[resource.ObjectId!].Actions, - Roles = rolesWithActions[resource.ObjectId!].Roles - }); - - return results; - } - } -} diff --git a/src/dotnet/Common/Extensions/ResourceProviderServiceExtensions.cs b/src/dotnet/Common/Extensions/ResourceProviderServiceExtensions.cs deleted file mode 100644 index 5068a173bb..0000000000 --- a/src/dotnet/Common/Extensions/ResourceProviderServiceExtensions.cs +++ /dev/null @@ -1,239 +0,0 @@ -using FoundationaLLM.Common.Constants.ResourceProviders; -using FoundationaLLM.Common.Exceptions; -using FoundationaLLM.Common.Interfaces; -using FoundationaLLM.Common.Models.Authentication; -using FoundationaLLM.Common.Models.ResourceProviders; -using System.Text.Json; - -namespace FoundationaLLM.Common.Extensions -{ - /// - /// Extends the interface with helper methods. - /// - public static class ResourceProviderServiceExtensions - { - /// - /// Creates or updates a resource. - /// - /// The object type of the resource being created or updated. - /// The object type of the response returned by the operation - /// The providing the resource provider services. - /// The FoundationaLLM instance ID. - /// The resource object. - /// The name of the resource type. - /// The providing information about the calling user identity. - /// A object with the result of the operation. - /// - public static async Task CreateOrUpdateResource( - this IResourceProviderService resourceProviderService, - string instanceId, - T resource, - string resourceTypeName, - UnifiedUserIdentity userIdentity) - where T : ResourceBase - where TResult : ResourceProviderUpsertResult - { - if (!resourceProviderService.IsInitialized) - throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} is not initialized."); - - var result = await resourceProviderService.UpsertResourceAsync( - $"/instances/{instanceId}/providers/{resourceProviderService.Name}/{resourceTypeName}/{resource.Name}", - resource, - userIdentity); - - return (result as TResult)!; - } - - /// - /// Gets a resource from the resource provider service. - /// - /// The object type of the resource being retrieved. - /// The providing the resource provider services. - /// The FoundationaLLM instance ID. - /// The resource name being checked. - /// The name of the resource type. - /// The providing information about the calling user identity. - /// A resource object of type . - public static async Task HandleGet( - this IResourceProviderService resourceProviderService, - string instanceId, - string resourceName, - string resourceTypeName, - UnifiedUserIdentity userIdentity) - where T : ResourceBase - { - if (!resourceProviderService.IsInitialized) - throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} is not initialized."); - - var result = await resourceProviderService.HandleGetAsync( - $"/instances/{instanceId}/providers/{resourceProviderService.Name}/{resourceTypeName}/{resourceName}", - userIdentity) as List>; - - if (result == null || result.Count == 0) - throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} is unable to retrieve the {resourceName} resource."); - - return result.First().Resource; - } - - /// - /// Gets a resource from the resource provider service. - /// - /// The object type of the resource being retrieved. - /// The providing the resource provider services. - /// The resource object identifier. - /// The providing information about the calling user identity. - /// A resource object of type . - public static async Task HandleGet( - this IResourceProviderService resourceProviderService, - string objectId, - UnifiedUserIdentity userIdentity) - where T : ResourceBase - { - if (!resourceProviderService.IsInitialized) - throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} is not initialized."); - - var result = await resourceProviderService.HandleGetAsync( - objectId, - userIdentity) as List>; - - if (result == null || result.Count == 0) - throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} is unable to retrieve the {objectId} resource."); - - return result.First().Resource; - } - - /// - /// Gets a list of resources from the resource provider service. - /// - /// The object type of the resources being retrieved. - /// The providing the resource provider services. - /// The providing information about the calling user identity. - /// A list of resource objects of type . - /// - public static async Task> GetResources( - this IResourceProviderService resourceProviderService, - UnifiedUserIdentity userIdentity) - where T : ResourceBase - { - if (!resourceProviderService.IsInitialized) - throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} is not initialized."); - - var resourceTypeDescriptor = resourceProviderService.AllowedResourceTypes.Values - .SingleOrDefault(rtd => rtd.TypeAllowedForHttpGet(typeof(ResourceProviderGetResult))) - ?? throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} does not support retrieving resources of type {typeof(T).Name}."); - - var result = await resourceProviderService.HandleGetAsync( - $"/{resourceTypeDescriptor.ResourceType}", - userIdentity); - - return (result as List>)!.Select(x => x.Resource).ToList(); - } - - /// - /// Gets a list of resources with RBAC information from the resource provider service. - /// - /// The object type of the resources being retrieved. - /// The providing the resource provider services. - /// The FoundationaLLM instance ID. - /// The providing information about the calling user identity. - /// A list of resource objects of type . - /// - public static async Task>> GetResourcesWithRBAC( - this IResourceProviderService resourceProviderService, - string instanceId, - UnifiedUserIdentity userIdentity) - where T : ResourceBase - { - if (!resourceProviderService.IsInitialized) - throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} is not initialized."); - - var resourceTypeDescriptor = resourceProviderService.AllowedResourceTypes.Values - .SingleOrDefault(rtd => rtd.TypeAllowedForHttpGet(typeof(ResourceProviderGetResult))) - ?? throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} does not support retrieving resources of type {typeof(T).Name}."); - - var result = await resourceProviderService.HandleGetAsync( - $"{resourceTypeDescriptor.ResourceType}", - userIdentity); - - return (result as List>)!; - } - - /// - /// Checks a resource name for availability. - /// - /// The providing the resource provider services. - /// The FoundationaLLM instance ID. - /// The resource name being checked. - /// The name of the resource type. - /// The providing information about the calling user identity. - /// A object with the result of the name check. - public static async Task CheckResourceName( - this IResourceProviderService resourceProviderService, - string instanceId, - string resourceName, - string resourceTypeName, - UnifiedUserIdentity userIdentity) - { - if (!resourceProviderService.IsInitialized) - throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} is not initialized."); - - var result = await resourceProviderService.HandlePostAsync( - $"/instances/{instanceId}/providers/{resourceProviderService.Name}/{resourceTypeName}/{ResourceProviderActions.CheckName}", - JsonSerializer.Serialize(new ResourceName { Name = resourceName }), - userIdentity); - - return (result as ResourceNameCheckResult)!; - } - - /// - /// Checks if a resource exists. - /// - /// The providing the resource provider services. - /// The FoundationaLLM instance ID. - /// The resource name being checked. - /// The name of the resource type. - /// The providing information about the calling user identity. - /// True if the resource exists, False otherwise. - /// - /// If a resource was logically deleted but not purged, this method will return True, indicating the existence of the resource. - /// - public static async Task ResourceExists( - this IResourceProviderService resourceProviderService, - string instanceId, - string resourceName, - string resourceTypeName, - UnifiedUserIdentity userIdentity) - { - if (!resourceProviderService.IsInitialized) - throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} is not initialized."); - - var result = await resourceProviderService.HandlePostAsync( - $"/instances/{instanceId}/providers/{resourceProviderService.Name}/{resourceTypeName}/{ResourceProviderActions.CheckName}", - JsonSerializer.Serialize(new ResourceName { Name = resourceName }), - userIdentity); - - return (result as ResourceNameCheckResult)!.Status == NameCheckResultType.Denied; - } - - /// - /// Waits for the resource provider service to be initialized. - /// - /// The providing the resource provider services. - /// - public static async Task WaitForInitialization( - this IResourceProviderService resourceProviderService) - { - if (resourceProviderService.IsInitialized) - return; - - for (int i = 0; i < 6; i++) - { - await Task.Delay(TimeSpan.FromSeconds(10)); - if (resourceProviderService.IsInitialized) - return; - } - - throw new ResourceProviderException($"The resource provider {resourceProviderService.Name} did not initialize within the expected time frame."); - } - } -} diff --git a/src/dotnet/Common/Interfaces/IAuthorizationService.cs b/src/dotnet/Common/Interfaces/IAuthorizationService.cs index 5fe028096d..8b5a748be0 100644 --- a/src/dotnet/Common/Interfaces/IAuthorizationService.cs +++ b/src/dotnet/Common/Interfaces/IAuthorizationService.cs @@ -1,4 +1,5 @@ -using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models; +using FoundationaLLM.Common.Models.Authentication; using FoundationaLLM.Common.Models.Authorization; namespace FoundationaLLM.Common.Interfaces @@ -14,58 +15,61 @@ public interface IAuthorizationService /// The FoundationaLLM instance id. /// The action identifier. /// The resource paths. + /// A value indicating whether to expand resource type paths that are not authorized. + /// A value indicating whether to include roles in the response. /// The user identity. + /// + /// + /// If the action specified by is not authorized for a resource type path, + /// and is set to true, the response will include + /// any authorized resource paths matching the resource type path. + /// + /// + /// If is set to true, for each authrorized resource path, + /// the response will include the roles assigned directly or indirectly to the resource path. + /// + /// /// An containing the result of the processing. Task ProcessAuthorizationRequest( string instanceId, string action, List resourcePaths, + bool expandResourceTypePaths, + bool includeRoles, UnifiedUserIdentity userIdentity); /// - /// Processes a role assignment request. + /// Creates a new role assignment. /// /// The FoundationaLLM instance identifier. - /// The role assignment request. + /// The containing the details of the role assignment to be created. /// The user identity. - /// - Task ProcessRoleAssignmentRequest( + /// A containing information about the result of the operation. + Task CreateRoleAssignment( string instanceId, RoleAssignmentRequest roleAssignmentRequest, UnifiedUserIdentity userIdentity); /// - /// Returns a list of role names and a list of allowed actions for the specified scope. - /// - /// The FoundationaLLM instance identifier. - /// The get roles with actions request. - /// The user identity. - /// The get roles and actions result. - Task> ProcessRoleAssignmentsWithActionsRequest( - string instanceId, - RoleAssignmentsWithActionsRequest request, - UnifiedUserIdentity userIdentity); - - /// - /// Returns a list of role assignments for the specified instance and resource. + /// Returns a list of role assignments. /// /// The FoundationaLLM instance identifier. /// The providing the inputs for filtering the role assignments. /// The user identity. /// The list of all role assignments for the specified instance. - Task> GetRoleAssignments( + Task> GetRoleAssignments( string instanceId, RoleAssignmentQueryParameters queryParameters, UnifiedUserIdentity userIdentity); /// - /// Revokes a role assignment for a specified instance. + /// Deletes a role assignment. /// /// The FoundationaLLM instance identifier. /// The role assignment object identifier. /// The user identity. - /// The role assignment result. - Task RevokeRoleAssignment( + /// A containing information about the result of the operation. + Task DeleteRoleAssignment( string instanceId, string roleAssignment, UnifiedUserIdentity userIdentity); diff --git a/src/dotnet/Core/Interfaces/ICosmosDbService.cs b/src/dotnet/Common/Interfaces/ICosmosDBService.cs similarity index 88% rename from src/dotnet/Core/Interfaces/ICosmosDbService.cs rename to src/dotnet/Common/Interfaces/ICosmosDBService.cs index 5f4bc81674..9860aac54f 100644 --- a/src/dotnet/Core/Interfaces/ICosmosDbService.cs +++ b/src/dotnet/Common/Interfaces/ICosmosDBService.cs @@ -1,12 +1,12 @@ -using FoundationaLLM.Common.Models.Chat; -using FoundationaLLM.Common.Models.Configuration.Users; +using FoundationaLLM.Common.Models.Configuration.Users; +using FoundationaLLM.Common.Models.Conversation; -namespace FoundationaLLM.Core.Interfaces; +namespace FoundationaLLM.Common.Interfaces; /// /// Contains methods for accessing Azure Cosmos DB for NoSQL. /// -public interface ICosmosDbService +public interface ICosmosDBService { /// /// Gets a list of all current chat sessions. @@ -16,7 +16,7 @@ public interface ICosmosDbService /// sessions for the signed in user. /// Cancellation token for async calls. /// List of distinct chat session items. - Task> GetSessionsAsync(string type, string upn, CancellationToken cancellationToken = default); + Task> GetSessionsAsync(string type, string upn, CancellationToken cancellationToken = default); /// /// Gets a list of all current chat messages for a specified session identifier. @@ -32,7 +32,7 @@ public interface ICosmosDbService /// Performs a point read to retrieve a single chat session item. /// /// The chat session item. - Task GetSessionAsync(string id, CancellationToken cancellationToken = default); + Task GetSessionAsync(string id, CancellationToken cancellationToken = default); /// /// Creates a new chat session. @@ -40,7 +40,7 @@ public interface ICosmosDbService /// Chat session item to create. /// Cancellation token for async calls. /// Newly created chat session item. - Task InsertSessionAsync(Session session, CancellationToken cancellationToken = default); + Task InsertSessionAsync(Conversation session, CancellationToken cancellationToken = default); /// /// Creates a new chat message. @@ -74,7 +74,7 @@ public interface ICosmosDbService /// Chat session item to update. /// Cancellation token for async calls. /// Revised created chat session item. - Task UpdateSessionAsync(Session session, CancellationToken cancellationToken = default); + Task UpdateSessionAsync(Conversation session, CancellationToken cancellationToken = default); /// /// Updates a session's name through a patch operation. @@ -83,7 +83,7 @@ public interface ICosmosDbService /// The session's new name. /// Cancellation token for async calls. /// Revised chat session item. - Task UpdateSessionNameAsync(string id, string sessionName, CancellationToken cancellationToken = default); + Task UpdateSessionNameAsync(string id, string sessionName, CancellationToken cancellationToken = default); /// /// Batch create or update chat messages and session. @@ -97,7 +97,7 @@ public interface ICosmosDbService /// The chat session item to create or replace. /// Cancellation token for async calls. /// - Task UpsertUserSessionAsync(Session session, CancellationToken cancellationToken = default); + Task UpsertUserSessionAsync(Conversation session, CancellationToken cancellationToken = default); /// /// Batch deletes an existing chat session and all related messages. diff --git a/src/dotnet/Common/Interfaces/IManagementProviderService.cs b/src/dotnet/Common/Interfaces/IManagementProviderService.cs index e648939fd2..822c076717 100644 --- a/src/dotnet/Common/Interfaces/IManagementProviderService.cs +++ b/src/dotnet/Common/Interfaces/IManagementProviderService.cs @@ -1,4 +1,5 @@ using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models.ResourceProviders; namespace FoundationaLLM.Common.Interfaces { @@ -12,8 +13,9 @@ public interface IManagementProviderService /// /// The resource path. /// The with details about the identity of the user. + /// The which provides operation parameters. /// The serialized form of the result of handling the request. - Task HandleGetAsync(string resourcePath, UnifiedUserIdentity userIdentity); + Task HandleGetAsync(string resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null); /// /// Handles a HTTP POST request for a specified resource path. diff --git a/src/dotnet/Common/Interfaces/IResourceProviderService.cs b/src/dotnet/Common/Interfaces/IResourceProviderService.cs index c7309364a8..391c70a1cd 100644 --- a/src/dotnet/Common/Interfaces/IResourceProviderService.cs +++ b/src/dotnet/Common/Interfaces/IResourceProviderService.cs @@ -33,33 +33,78 @@ public interface IResourceProviderService : IManagementProviderService /// string StorageContainerName { get; } + /// + /// Gets resources of a specific type. + /// + /// The type of resource to return. + /// The FoundationaLLM instance id. + /// The with details about the identity of the user. + /// The which provides operation parameters. + /// A list of containing the loaded resources. + /// + Task>> GetResourcesAsync( + string instanceId, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) + where T : ResourceBase; + /// /// Gets a resource based on its logical path. /// /// The type of the resource. /// The logical path of the resource. /// The with details about the identity of the user. - /// The which provides operation parameters. + /// The which provides operation parameters. /// The instance of the resource corresponding to the specified logical path. - Task GetResource(string resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderOptions? options = null) where T : class; + Task GetResourceAsync(string resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) + where T : ResourceBase; + + /// + /// Gets a resource based on its name. + /// + /// The type of the resource. + /// The FoundationaLLM instance id. + /// The logical path of the resource. + /// The with details about the identity of the user. + /// The which provides operation parameters. + /// The instance of the resource corresponding to the specified logical path. + Task GetResourceAsync(string instanceId, string resourceName, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) + where T : ResourceBase; /// /// Creates or updates a resource based on its logical path. /// /// The type of the resource. /// The type of the result returned - /// The logical path of the resource. + /// The FoundationaLLM instance id. /// The instance of the resource being created or updated. /// The with details about the identity of the user. /// The object id of the resource. - Task UpsertResourceAsync(string resourcePath, T resource, UnifiedUserIdentity userIdentity) + Task UpsertResourceAsync(string instanceId, T resource, UnifiedUserIdentity userIdentity) where T : ResourceBase where TResult : ResourceProviderUpsertResult; + /// + /// Checks if a resource exists. + /// + /// The type of the resource. + /// The FoundationaLLM instance ID. + /// The resource name being checked. + /// The providing information about the calling user identity. + /// A tuple indicating whether the resource exists or not and whether it is logically deleted or not. + /// + /// If a resource was logically deleted but not purged, this method will return True, indicating the existence of the resource. + /// + Task<(bool Exists, bool Deleted)> ResourceExists(string instanceId, string resourceName, UnifiedUserIdentity userIdentity) + where T : ResourceBase; + /// /// Initializes the resource provider. /// /// Task Initialize(); + + /// + /// Waits for the resource provider service to be initialized. + /// + Task WaitForInitialization(); } } diff --git a/src/dotnet/Common/Models/Authorization/ActionAuthorizationRequest.cs b/src/dotnet/Common/Models/Authorization/ActionAuthorizationRequest.cs index d6a1bb1594..4aad9b9c43 100644 --- a/src/dotnet/Common/Models/Authorization/ActionAuthorizationRequest.cs +++ b/src/dotnet/Common/Models/Authorization/ActionAuthorizationRequest.cs @@ -20,15 +20,26 @@ public class ActionAuthorizationRequest public required List ResourcePaths { get; set; } /// - /// The id of the security principal requesting authorization. + /// Gets or sets a value indicating whether to expand resource type paths that are not authorized. /// - [JsonPropertyName("principal_id")] - public required string PrincipalId { get; set; } + /// + /// If the action specified by is not authorized for a resource type path, and this property is set to true, the response will include any authorized resource paths matching the resource type path. + /// + public required bool ExpandResourceTypePaths { get; set; } /// - /// The list of security group ids to which the principal belongs. + /// Gets or sets a value indicating whether to include roles in the response. /// - [JsonPropertyName("security_group_ids")] - public List SecurityGroupIds { get; set; } = []; + /// + /// If this property is set to true, for each authrorized resource path, + /// the response will include the roles assigned directly or indirectly to the resource path. + /// + public required bool IncludeRoles { get; set; } + + /// + /// The containing the authorization context for the user. + /// + [JsonPropertyName("user_context")] + public required UserAuthorizationContext UserContext { get; set; } } } diff --git a/src/dotnet/Common/Models/Authorization/ActionAuthorizationResult.cs b/src/dotnet/Common/Models/Authorization/ActionAuthorizationResult.cs index 3496d2b04a..d04eb95c4d 100644 --- a/src/dotnet/Common/Models/Authorization/ActionAuthorizationResult.cs +++ b/src/dotnet/Common/Models/Authorization/ActionAuthorizationResult.cs @@ -8,13 +8,13 @@ namespace FoundationaLLM.Common.Models.Authorization public class ActionAuthorizationResult { /// - /// Indicates whether the action is authorized or not for each resource path. + /// Gets or sets the dictionary containing objects representing the authorization result for each resource path. /// [JsonPropertyName("authorization_results")] - public required Dictionary AuthorizationResults { get; set; } + public required Dictionary AuthorizationResults { get; set; } /// - /// Contains a list of invalid resource paths, for which authorization could not be completed. + /// Gets or sets a list of invalid resource paths, for which authorization could not be completed. /// [JsonPropertyName("invalid_resources")] public List? InvalidResourcePaths { get; set; } diff --git a/src/dotnet/Common/Models/Authorization/ResourcePathAuthorizationResult.cs b/src/dotnet/Common/Models/Authorization/ResourcePathAuthorizationResult.cs new file mode 100644 index 0000000000..f6962391d0 --- /dev/null +++ b/src/dotnet/Common/Models/Authorization/ResourcePathAuthorizationResult.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; + +namespace FoundationaLLM.Common.Models.Authorization +{ + /// + /// Represents the result of a resource path authorization request. + /// + public class ResourcePathAuthorizationResult + { + /// + /// Gets or sets the resource path that was authorized. + /// + [JsonPropertyName("resource_path")] + public required string ResourcePath { get; set; } + + /// + /// Gets or sets the name of the resource that was authorized. + /// + [JsonPropertyName("resource_name")] + public string? ResourceName { get; set; } + + /// + /// Gets or sets a value indicating whether the resource path is authorized. + /// + [JsonPropertyName("authorized")] + public bool Authorized { get; set; } + + /// + /// Gets or sets the list of roles that authorize the action for the resource path. + /// + /// + /// The list contains the display names of the roles (e.g., Reader, Contributor, Owner, etc.). + /// + [JsonPropertyName("roles")] + public List Roles { get; set; } = []; + + /// + /// Gets or sets the dictionary of objects representing + /// authorization results for subordinate resource paths. They keys of the dictionary are the resource names. + /// + /// + /// This dictionary will only contain values if the resource path in is + /// a resource type path and + /// was set to true on the request that generated this result. + /// + [JsonPropertyName("subordinate_resource_paths_authorization_results")] + public Dictionary SubordinateResourcePathsAuthorizationResults { get; set; } = []; + + /// + /// Gets or sets a value indicating whether an owner role assignment must be set for the resource. + /// + public bool MustSetOwnerRoleAssignment { get; set; } = true; + } +} diff --git a/src/dotnet/Common/Models/Authorization/RoleAssignmentResult.cs b/src/dotnet/Common/Models/Authorization/RoleAssignmentOperationResult.cs similarity index 59% rename from src/dotnet/Common/Models/Authorization/RoleAssignmentResult.cs rename to src/dotnet/Common/Models/Authorization/RoleAssignmentOperationResult.cs index f8c94760c9..77988a9a77 100644 --- a/src/dotnet/Common/Models/Authorization/RoleAssignmentResult.cs +++ b/src/dotnet/Common/Models/Authorization/RoleAssignmentOperationResult.cs @@ -3,12 +3,12 @@ namespace FoundationaLLM.Common.Models.Authorization { /// - /// Represents the result of a role assignment request. + /// Represents the result of a role assignment operation. /// - public class RoleAssignmentResult + public class RoleAssignmentOperationResult { /// - /// Indicates whether the role assignment was successful or not. + /// Indicates whether the role assignment operation was successful or not. /// [JsonPropertyName("success")] public required bool Success { get; set; } diff --git a/src/dotnet/Common/Models/Authorization/UserAuthorizationContext.cs b/src/dotnet/Common/Models/Authorization/UserAuthorizationContext.cs new file mode 100644 index 0000000000..9afeb0ea6e --- /dev/null +++ b/src/dotnet/Common/Models/Authorization/UserAuthorizationContext.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace FoundationaLLM.Common.Models.Authorization +{ + /// + /// Represents authorization context for a user. + /// + public class UserAuthorizationContext + { + /// + /// The unique identifier of the user. + /// + [JsonPropertyName("security_principal_id")] + public required string SecurityPrincipalId { get; set; } + + /// + /// The user principal name (UPN). + /// + [JsonPropertyName("user_principal_name")] + public required string UserPrincipalName { get; set; } + + /// + /// The list of security group identifiers to which the user belongs. + /// + [JsonPropertyName("security_group_ids")] + public required List SecurityGroupIds { get; set; } = []; + } +} diff --git a/src/dotnet/Common/Models/Chat/AttachmentDetail.cs b/src/dotnet/Common/Models/Chat/AttachmentDetail.cs deleted file mode 100644 index 66521b55a4..0000000000 --- a/src/dotnet/Common/Models/Chat/AttachmentDetail.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Text.Json.Serialization; - -namespace FoundationaLLM.Common.Models.Chat -{ - /// - /// Represents an attachment in a chat message or session. - /// - public class AttachmentDetail - { - /// - /// The unique identifier of the attachment resource. - /// - [JsonPropertyName("objectId")] - public string? ObjectId { get; set; } - - /// - /// The attachment file name. - /// - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } - - /// - /// The mime content type of the attachment. - /// - [JsonPropertyName("contentType")] - public string? ContentType { get; set; } - } -} diff --git a/src/dotnet/Common/Models/Conversation/AttachmentDetail.cs b/src/dotnet/Common/Models/Conversation/AttachmentDetail.cs new file mode 100644 index 0000000000..948f6efd53 --- /dev/null +++ b/src/dotnet/Common/Models/Conversation/AttachmentDetail.cs @@ -0,0 +1,42 @@ +using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Models.ResourceProviders.Attachment; +using System.Text.Json.Serialization; + +namespace FoundationaLLM.Common.Models.Conversation +{ + /// + /// Represents an attachment in a chat message or session. + /// + public class AttachmentDetail + { + /// + /// The unique identifier of the attachment resource. + /// + [JsonPropertyName("objectId")] + public string? ObjectId { get; set; } + + /// + /// The attachment file name. + /// + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// + /// The mime content type of the attachment. + /// + [JsonPropertyName("contentType")] + public string? ContentType { get; set; } + + /// + /// Creates an instance from an instance. + /// + /// The used to initialize the instance. + /// The newly created instance. + public static AttachmentDetail FromAttachmentFile(AttachmentFile attachmentFile) => new() + { + ObjectId = attachmentFile.ObjectId, + DisplayName = !string.IsNullOrWhiteSpace(attachmentFile.DisplayName) ? attachmentFile.DisplayName : attachmentFile.OriginalFileName, + ContentType = attachmentFile.ContentType + }; + } +} diff --git a/src/dotnet/Common/Models/Chat/ChatSessionProperties.cs b/src/dotnet/Common/Models/Conversation/ChatSessionProperties.cs similarity index 86% rename from src/dotnet/Common/Models/Chat/ChatSessionProperties.cs rename to src/dotnet/Common/Models/Conversation/ChatSessionProperties.cs index 97e1384bf0..ab2777f2c9 100644 --- a/src/dotnet/Common/Models/Chat/ChatSessionProperties.cs +++ b/src/dotnet/Common/Models/Conversation/ChatSessionProperties.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace FoundationaLLM.Common.Models.Chat +namespace FoundationaLLM.Common.Models.Conversation { /// /// The session properties object. diff --git a/src/dotnet/Common/Models/Chat/Completion.cs b/src/dotnet/Common/Models/Conversation/Completion.cs similarity index 80% rename from src/dotnet/Common/Models/Chat/Completion.cs rename to src/dotnet/Common/Models/Conversation/Completion.cs index 2319f89958..d267bb358e 100644 --- a/src/dotnet/Common/Models/Chat/Completion.cs +++ b/src/dotnet/Common/Models/Conversation/Completion.cs @@ -1,4 +1,4 @@ -namespace FoundationaLLM.Common.Models.Chat +namespace FoundationaLLM.Common.Models.Conversation { /// /// The completion object. diff --git a/src/dotnet/Common/Models/Chat/CompletionPrompt.cs b/src/dotnet/Common/Models/Conversation/CompletionPrompt.cs similarity index 96% rename from src/dotnet/Common/Models/Chat/CompletionPrompt.cs rename to src/dotnet/Common/Models/Conversation/CompletionPrompt.cs index 622f1f7d89..a167546d4c 100644 --- a/src/dotnet/Common/Models/Chat/CompletionPrompt.cs +++ b/src/dotnet/Common/Models/Conversation/CompletionPrompt.cs @@ -1,6 +1,6 @@ using FoundationaLLM.Common.Models.Orchestration.Response; -namespace FoundationaLLM.Common.Models.Chat +namespace FoundationaLLM.Common.Models.Conversation { /// /// The completion prompt object. diff --git a/src/dotnet/Common/Models/Chat/DocumentVector.cs b/src/dotnet/Common/Models/Conversation/DocumentVector.cs similarity index 95% rename from src/dotnet/Common/Models/Chat/DocumentVector.cs rename to src/dotnet/Common/Models/Conversation/DocumentVector.cs index 5cb123851b..fb4e4b3d2e 100644 --- a/src/dotnet/Common/Models/Chat/DocumentVector.cs +++ b/src/dotnet/Common/Models/Conversation/DocumentVector.cs @@ -1,4 +1,4 @@ -namespace FoundationaLLM.Common.Models.Chat +namespace FoundationaLLM.Common.Models.Conversation { /// /// The document vector object. diff --git a/src/dotnet/Common/Models/Chat/Message.cs b/src/dotnet/Common/Models/Conversation/Message.cs similarity index 98% rename from src/dotnet/Common/Models/Chat/Message.cs rename to src/dotnet/Common/Models/Conversation/Message.cs index a9fb7f82bf..ebb8740751 100644 --- a/src/dotnet/Common/Models/Chat/Message.cs +++ b/src/dotnet/Common/Models/Conversation/Message.cs @@ -2,7 +2,7 @@ using FoundationaLLM.Common.Models.Orchestration.Response; using System.Text.Json.Serialization; -namespace FoundationaLLM.Common.Models.Chat; +namespace FoundationaLLM.Common.Models.Conversation; /// /// The message object. diff --git a/src/dotnet/Common/Models/Chat/MessageContent.cs b/src/dotnet/Common/Models/Conversation/MessageContent.cs similarity index 93% rename from src/dotnet/Common/Models/Chat/MessageContent.cs rename to src/dotnet/Common/Models/Conversation/MessageContent.cs index 30484acb04..f73e8bd74d 100644 --- a/src/dotnet/Common/Models/Chat/MessageContent.cs +++ b/src/dotnet/Common/Models/Conversation/MessageContent.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace FoundationaLLM.Common.Models.Chat +namespace FoundationaLLM.Common.Models.Conversation { /// /// Contains parts that compose the message content. diff --git a/src/dotnet/Common/Models/Chat/MessageHistoryItem.cs b/src/dotnet/Common/Models/Conversation/MessageHistoryItem.cs similarity index 94% rename from src/dotnet/Common/Models/Chat/MessageHistoryItem.cs rename to src/dotnet/Common/Models/Conversation/MessageHistoryItem.cs index eeda2c48c1..1aeaf0f858 100644 --- a/src/dotnet/Common/Models/Chat/MessageHistoryItem.cs +++ b/src/dotnet/Common/Models/Conversation/MessageHistoryItem.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace FoundationaLLM.Common.Models.Chat +namespace FoundationaLLM.Common.Models.Conversation { /// /// Represents an historic message sender and text item. diff --git a/src/dotnet/Common/Models/Orchestration/Request/CompletionRequestBase.cs b/src/dotnet/Common/Models/Orchestration/Request/CompletionRequestBase.cs index ffc7ead12b..f6bc48c940 100644 --- a/src/dotnet/Common/Models/Orchestration/Request/CompletionRequestBase.cs +++ b/src/dotnet/Common/Models/Orchestration/Request/CompletionRequestBase.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using System.Text.Json.Serialization; namespace FoundationaLLM.Common.Models.Orchestration.Request diff --git a/src/dotnet/Common/Models/Chat/Session.cs b/src/dotnet/Common/Models/ResourceProviders/Conversation/Conversation.cs similarity index 82% rename from src/dotnet/Common/Models/Chat/Session.cs rename to src/dotnet/Common/Models/ResourceProviders/Conversation/Conversation.cs index 170af78676..c464759fcb 100644 --- a/src/dotnet/Common/Models/Chat/Session.cs +++ b/src/dotnet/Common/Models/ResourceProviders/Conversation/Conversation.cs @@ -1,11 +1,13 @@ +using FoundationaLLM.Common.Constants.Chat; +using FoundationaLLM.Common.Models.ResourceProviders; using System.Text.Json.Serialization; -namespace FoundationaLLM.Common.Models.Chat; +namespace FoundationaLLM.Common.Models.Conversation; /// /// The session object. /// -public record Session +public class Conversation : ResourceBase { /// /// The unique identifier. @@ -14,7 +16,7 @@ public record Session /// /// The type of the session. /// - public string Type { get; set; } + public new string Type { get; set; } /// /// The Partition key. @@ -27,7 +29,7 @@ public record Session /// /// The name of the session. /// - public string Name { get; set; } + public override required string Name { get; set; } /// /// The UPN of the user who created the chat session. /// @@ -35,7 +37,7 @@ public record Session /// /// Deleted flag used for soft delete. /// - public bool Deleted { get; set; } + public override bool Deleted { get; set; } /// /// The list of messages associated with the session. /// @@ -45,10 +47,10 @@ public record Session /// /// Constructor for Session. /// - public Session() + public Conversation() { Id = Guid.NewGuid().ToString(); - Type = nameof(Session); + Type = ConversationTypes.Session; SessionId = Id; TokensUsed = 0; Name = "New Chat"; diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourceBase.cs b/src/dotnet/Common/Models/ResourceProviders/ResourceBase.cs index 4b95b168b3..7ef7a45794 100644 --- a/src/dotnet/Common/Models/ResourceProviders/ResourceBase.cs +++ b/src/dotnet/Common/Models/ResourceProviders/ResourceBase.cs @@ -36,28 +36,28 @@ public class ResourceBase : ResourceName public string? CostCenter { get; set; } /// - /// The time at which the security role definition was created. + /// The time at which the resource was created. /// [JsonPropertyName("created_on")] [JsonPropertyOrder(500)] public DateTimeOffset CreatedOn { get; set; } /// - /// The time at which the security role definition was last updated. + /// The time at which the resource was last updated. /// [JsonPropertyName("updated_on")] [JsonPropertyOrder(501)] public DateTimeOffset UpdatedOn { get; set; } /// - /// The entity who created the security role definition. + /// The entity who created the resource. /// [JsonPropertyName("created_by")] [JsonPropertyOrder(502)] public string? CreatedBy { get; set; } /// - /// The entity who last updated the security role definition. + /// The entity who last updated the resource. /// [JsonPropertyName("updated_by")] [JsonPropertyOrder(503)] diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourceFilter.cs b/src/dotnet/Common/Models/ResourceProviders/ResourceFilter.cs index 0065d0c34b..04a8d2c4ec 100644 --- a/src/dotnet/Common/Models/ResourceProviders/ResourceFilter.cs +++ b/src/dotnet/Common/Models/ResourceProviders/ResourceFilter.cs @@ -8,15 +8,22 @@ namespace FoundationaLLM.Common.Models.ResourceProviders public class ResourceFilter { /// - /// Specify whether to filter by resources designated as default. - /// If null, the filter will not be applied. If true, only default resources will be returned. + /// Gets or sets a value that specifies whether the default resource should be retrieved or not. /// + /// + /// If set, this value has precedence over the property. + /// If not set, the property is used to filter resources. + /// [JsonPropertyName("default")] - public bool? Default { get; set; } + public bool? DefaultResource { get; set; } /// - /// Retrieve resources or resource references that match the list of Object IDs. + /// Gets or sets a list of object IDs to filter resources. /// + /// + /// The property has precendece over this property. + /// If the property is set, this property is ignored. + /// [JsonPropertyName("object_ids")] public List? ObjectIDs { get; set; } } diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourceName.cs b/src/dotnet/Common/Models/ResourceProviders/ResourceName.cs index 0c156f49dc..3123e238e5 100644 --- a/src/dotnet/Common/Models/ResourceProviders/ResourceName.cs +++ b/src/dotnet/Common/Models/ResourceProviders/ResourceName.cs @@ -19,6 +19,6 @@ public class ResourceName /// [JsonPropertyName("name")] [JsonPropertyOrder(-5)] - public required string Name { get; set; } + public virtual required string Name { get; set; } } } diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourceNameCheckResult.cs b/src/dotnet/Common/Models/ResourceProviders/ResourceNameCheckResult.cs index 4fb0f7f8b7..056af7cffb 100644 --- a/src/dotnet/Common/Models/ResourceProviders/ResourceNameCheckResult.cs +++ b/src/dotnet/Common/Models/ResourceProviders/ResourceNameCheckResult.cs @@ -17,6 +17,20 @@ public class ResourceNameCheckResult : ResourceName /// An optional message indicating why is the name not allowed. /// public string? Message { get; set; } + + /// + /// Gets or sets a value indicating whether the resource exists or not. + /// + /// + /// For logically deleted resources, the value of this property will be true. + /// The property indicates whether the resource was logically deleted or not. + /// + public required bool Exists { get; set; } + + /// + /// Gets or sets a value indicating whether the resource is logically deleted or not. + /// + public required bool Deleted { get; set; } } /// diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourcePath.cs b/src/dotnet/Common/Models/ResourceProviders/ResourcePath.cs index 6a2ba7b5d7..752fe2434d 100644 --- a/src/dotnet/Common/Models/ResourceProviders/ResourcePath.cs +++ b/src/dotnet/Common/Models/ResourceProviders/ResourcePath.cs @@ -14,6 +14,7 @@ public class ResourcePath private readonly string? _resourceProvider; private readonly List _resourceTypeInstances; private readonly bool _isRootPath; + private readonly string _rawResourcePath; private const string INSTANCE_TOKEN = "instances"; private const string RESOURCE_PROVIDER_TOKEN = "providers"; @@ -38,6 +39,13 @@ public class ResourcePath /// public bool IsRootPath => _isRootPath; + /// + /// Indicates whether the resource path is an instance path or not (i.e., only contains the FoundationaLLM instance identifier). + /// + public bool IsInstancePath => + !(_instanceId == null) + && (_resourceProvider == null); + /// /// Indicates whether the resource path refers to a resource type (does not contain a resource name). /// @@ -47,19 +55,100 @@ public class ResourcePath && _resourceTypeInstances.Last().ResourceId == null; /// - /// The main resource type of the path. + /// Gets the name of the main resource type of the path. + /// + /// + /// The main resource type is the first resource type in the path. In the case of nested resources, this will be the resource type of the main resource. + /// + public string? MainResourceTypeName => + _resourceTypeInstances == null || _resourceTypeInstances.Count == 0 + ? null + : _resourceTypeInstances[0].ResourceTypeName; + + /// + /// Gets the object type of the main resource type of the path. /// - public string? MainResourceType => + /// + /// The main resource type is the first resource type in the path. In the case of nested resources, this will be the resource type of the main resource. + /// + public Type? MainResourceType => _resourceTypeInstances == null || _resourceTypeInstances.Count == 0 ? null : _resourceTypeInstances[0].ResourceType; /// - /// Indicates whether the resource path is an instance path or not (i.e., only contains the FoundationaLLM instance identifier). + /// Gets the resource id of the main resource type of the path. /// - public bool IsInstancePath => - !(_instanceId == null) - && (_resourceProvider == null); + public string? MainResourceId => + _resourceTypeInstances == null || _resourceTypeInstances.Count == 0 + ? null + : _resourceTypeInstances[0].ResourceId; + + /// + /// Gets the resource type name of the resource identified by the path. + /// + /// + /// This is the last resource type name in the path. If the path refers to nested resources, this will be the resource type name of the last resource. + /// Otherwise, it will be the resource type name of the main resource (and hence identical to ). + /// + public string? ResourceTypeName => + _resourceTypeInstances == null || _resourceTypeInstances.Count == 0 + ? null + : _resourceTypeInstances.Last().ResourceTypeName; + + /// + /// Gets the resource type of the resource identified by the path. + /// + /// + /// This is the last resource type in the path. If the path refers to nested resources, this will be the resource type of the last resource. + /// Otherwise, it will be the resource type of the main resource (and hence identical to ). + /// + public Type? ResourceType => + _resourceTypeInstances == null || _resourceTypeInstances.Count == 0 + ? null + : _resourceTypeInstances.Last().ResourceType; + + /// + /// Gets the resource id of the resource identified by the path. + /// + /// + /// This is the last resource id in the path. If the path refers to nested resources, this will be the resource id of the last resource. + /// Otherwise, it will be the resource id of the main resource (and hence identical to ). + /// + public string? ResourceId => + _resourceTypeInstances == null || _resourceTypeInstances.Count == 0 + ? null + : _resourceTypeInstances.Last().ResourceId; + + /// + /// Indicates whether the resource path contains a valid resource id or not. + /// + public bool HasResourceId => + _resourceTypeInstances != null + && _resourceTypeInstances.Count > 0 + && !string.IsNullOrWhiteSpace(_resourceTypeInstances.Last().ResourceId); + + /// + /// Gets the action (if any) specified in the resource path. + /// + public string? Action => + _resourceTypeInstances == null || _resourceTypeInstances.Count == 0 + ? null + : _resourceTypeInstances.Last().Action; + + /// + /// Indicates whether the resource path contains a valid action or not. + /// + public bool HasAction => + _resourceTypeInstances != null + && _resourceTypeInstances.Count > 0 + && !string.IsNullOrWhiteSpace(_resourceTypeInstances.Last().Action); + + /// + /// Gets the raw resource path which was used to create the resource path object. + /// + public string RawResourcePath => + _rawResourcePath; /// /// Creates a new resource identifier from a resource path optionally allowing an action. @@ -72,7 +161,10 @@ public ResourcePath( string resourcePath, ImmutableList allowedResourceProviders, Dictionary allowedResourceTypes, - bool allowAction = true) => + bool allowAction = true) + { + _rawResourcePath = resourcePath; + ParseResourcePath( resourcePath, allowedResourceProviders, @@ -82,6 +174,7 @@ public ResourcePath( out _instanceId, out _resourceProvider, out _resourceTypeInstances); + } /// /// Tries to parse a resource path and create a resource identifier from it. @@ -157,7 +250,7 @@ public string GetObjectId( StatusCodes.Status400BadRequest); else return $"/instances/{instanceId}/providers/{resourceProvider}/{string.Join("/", - _resourceTypeInstances.Select(i => i.ResourceId == null ? $"{i.ResourceType}" : $"{i.ResourceType}/{i.ResourceId}").ToArray())}"; + _resourceTypeInstances.Select(i => i.ResourceId == null ? $"{i.ResourceTypeName}" : $"{i.ResourceTypeName}/{i.ResourceId}").ToArray())}"; } else { @@ -169,7 +262,7 @@ public string GetObjectId( StatusCodes.Status400BadRequest); else return $"/instances/{_instanceId}/providers/{_resourceProvider}/{string.Join("/", - _resourceTypeInstances.Select(i => i.ResourceId == null ? $"{i.ResourceType}" : $"{i.ResourceType}/{i.ResourceId}").ToArray())}"; + _resourceTypeInstances.Select(i => i.ResourceId == null ? $"{i.ResourceTypeName}" : $"{i.ResourceTypeName}/{i.ResourceId}").ToArray())}"; } } @@ -192,13 +285,14 @@ public static string GetObjectId( /// Determines whether the resource path includes another specified resource path. /// /// The to check for inclusion. + /// Indicates whether an equal resource path is considered to be included or not. /// True if the specified resource path is included in the resource path. - public bool IncludesResourcePath(ResourcePath other) + public bool IncludesResourcePath(ResourcePath other, bool allowEqual = true) { if (_isRootPath) // The other path is included only if it is root. return - other.IsRootPath; + other.IsRootPath && allowEqual; if (IsInstancePath) { @@ -208,7 +302,7 @@ public bool IncludesResourcePath(ResourcePath other) // An instance path includes another instance path for the same instance id. if (other.IsInstancePath) - return _instanceId == other.InstanceId; + return (_instanceId == other.InstanceId) && allowEqual; } // A full path includes a root path or an instance path. @@ -225,6 +319,29 @@ public bool IncludesResourcePath(ResourcePath other) return false; } + return + allowEqual + || StringComparer.OrdinalIgnoreCase.Compare(_rawResourcePath, other.RawResourcePath) != 0; + } + + /// + /// Determines whether the resource path matches exactly (including order) the resource types of another specified resource path. + /// + /// The to be matched. + /// True if the resource path matches exactly (including order) the resource types of the other resource path. + public bool MatchesResourceTypes(ResourcePath other) + { + if (_resourceTypeInstances.Count == 0 + || other.ResourceTypeInstances.Count == 0 + || _resourceTypeInstances.Count != other.ResourceTypeInstances.Count) + return false; + + for (int i = 0; i < _resourceTypeInstances.Count; i++) + if (!StringComparer.OrdinalIgnoreCase.Equals( + _resourceTypeInstances[i].ResourceTypeName, + other.ResourceTypeInstances[i].ResourceTypeName)) + return false; + return true; } @@ -297,8 +414,10 @@ private void ParseResourcePath( if (currentResourceTypes == null || !currentResourceTypes.TryGetValue(tokens[currentIndex], out ResourceTypeDescriptor? currentResourceType)) throw new Exception(); - - var resourceTypeInstance = new ResourceTypeInstance(tokens[currentIndex]); + + var resourceTypeInstance = new ResourceTypeInstance( + tokens[currentIndex], + currentResourceTypes[tokens[currentIndex]].ResourceType); resourceTypeInstances.Add(resourceTypeInstance); if (currentIndex + 1 == tokens.Length) diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourceProviderGetResult.cs b/src/dotnet/Common/Models/ResourceProviders/ResourceProviderGetResult.cs index e4a747b620..55d93c01bf 100644 --- a/src/dotnet/Common/Models/ResourceProviders/ResourceProviderGetResult.cs +++ b/src/dotnet/Common/Models/ResourceProviders/ResourceProviderGetResult.cs @@ -13,12 +13,6 @@ public class ResourceProviderGetResult where T : ResourceBase [JsonPropertyName("resource")] public required T Resource { get; set; } - /// - /// List of authorized actions on the resource. - /// - [JsonPropertyName("actions")] - public required List Actions { get; set; } - /// /// List of roles on the resource. /// diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourceProviderLoadOptions.cs b/src/dotnet/Common/Models/ResourceProviders/ResourceProviderLoadOptions.cs new file mode 100644 index 0000000000..bc332d4d92 --- /dev/null +++ b/src/dotnet/Common/Models/ResourceProviders/ResourceProviderLoadOptions.cs @@ -0,0 +1,24 @@ +namespace FoundationaLLM.Common.Models.ResourceProviders +{ + /// + /// Options for the resource provider requests. + /// + public class ResourceProviderLoadOptions + { + /// + /// Gets or sets a value indicating whether to load resource content (applicable only to resources that have content). + /// + public bool LoadContent { get; set; } + + /// + /// Gets or sets a value indicating whether to include roles in the response. + /// + /// + /// + /// If the value is set to true, for each resource, the response will include + /// the roles assigned directly or indirectly to the resource. + /// + /// + public bool IncludeRoles { get; set; } + } +} diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourceProviderOptions.cs b/src/dotnet/Common/Models/ResourceProviders/ResourceProviderOptions.cs deleted file mode 100644 index 772d76239d..0000000000 --- a/src/dotnet/Common/Models/ResourceProviders/ResourceProviderOptions.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace FoundationaLLM.Common.Models.ResourceProviders -{ - /// - /// Options for the resource provider requests. - /// - public class ResourceProviderOptions - { - /// - /// Indicates whether to load content such as files or images. - /// - public bool LoadContent { get; set; } - } -} diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourceProviderUpsertResult.cs b/src/dotnet/Common/Models/ResourceProviders/ResourceProviderUpsertResult.cs index 957bee7bdc..35d756573a 100644 --- a/src/dotnet/Common/Models/ResourceProviders/ResourceProviderUpsertResult.cs +++ b/src/dotnet/Common/Models/ResourceProviders/ResourceProviderUpsertResult.cs @@ -8,11 +8,11 @@ public class ResourceProviderUpsertResult /// /// The id of the object that was created or updated. /// - public string? ObjectId { get; set; } + public required string ObjectId { get; set; } /// /// A flag denoting whether the upserted resource already exists. /// - public bool? ResourceExists { get; set; } + public required bool ResourceExists { get; set; } } } diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourceReferenceList`1.cs b/src/dotnet/Common/Models/ResourceProviders/ResourceReferenceList`1.cs index a56148744a..2e6d2d32bc 100644 --- a/src/dotnet/Common/Models/ResourceProviders/ResourceReferenceList`1.cs +++ b/src/dotnet/Common/Models/ResourceProviders/ResourceReferenceList`1.cs @@ -10,5 +10,10 @@ public class ResourceReferenceList where T : ResourceReference /// The dictionary of resource references indexed by their unique names. /// public required List ResourceReferences { get; set; } + + /// + /// Gets or sets the name of the resource that should be used as the default resource. + /// + public string? DefaultResourceName { get; set; } } } diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourceTypeDescriptor.cs b/src/dotnet/Common/Models/ResourceProviders/ResourceTypeDescriptor.cs index 30044dff14..7e4d35fae0 100644 --- a/src/dotnet/Common/Models/ResourceProviders/ResourceTypeDescriptor.cs +++ b/src/dotnet/Common/Models/ResourceProviders/ResourceTypeDescriptor.cs @@ -3,14 +3,21 @@ /// /// Provides details about a resource type managed by a resource provider. /// - /// The name of the resource type. + /// The name of the resource type. + /// The object type of the resource type. public class ResourceTypeDescriptor( - string resourceType) + string resourceTypeName, + Type resourceType) { /// /// The name of the resource type. /// - public string ResourceType { get; set; } = resourceType; + public string ResourceTypeName { get; set; } = resourceTypeName; + + /// + /// The object type of the resource type. + /// + public Type ResourceType { get; set; } = resourceType; /// /// The list of actions supported by the resource type. @@ -57,11 +64,13 @@ public record ResourceTypeAction( /// Provides details about the types that are allowed for the body and return of a specific HTTP method. /// /// The name of the HTTP method. Can be one of GET, POST, PUT, PATCH, or DELETE. + /// The name of the authorizable operation that is required to authorize the request. /// The dictionary of query parameter names and types that are allowed for the method. /// The list of types that are allowed as payloads for the HTTP request. /// The list of types the are allowed as return types for the HTTP request. public record ResourceTypeAllowedTypes( string HttpMethod, + string AuthorizableOperation, Dictionary AllowedParameterTypes, List AllowedBodyTypes, List AllowedReturnTypes); diff --git a/src/dotnet/Common/Models/ResourceProviders/ResourceTypeInstance.cs b/src/dotnet/Common/Models/ResourceProviders/ResourceTypeInstance.cs index 411da5da2e..0e5050baa7 100644 --- a/src/dotnet/Common/Models/ResourceProviders/ResourceTypeInstance.cs +++ b/src/dotnet/Common/Models/ResourceProviders/ResourceTypeInstance.cs @@ -3,9 +3,11 @@ /// /// Identifies a specific resource type instance. /// - /// The name of the resource type. + /// The name of the resource type. + /// The object type of the resource type. public record ResourceTypeInstance( - string ResourceType) + string ResourceTypeName, + Type ResourceType) { /// /// An optional resource type instance unique identifier. @@ -32,7 +34,7 @@ public bool Includes(ResourceTypeInstance? other) return true; // Resource type instances with different resource types - if (!ResourceType.Equals(other.ResourceType)) + if (!ResourceTypeName.Equals(other.ResourceTypeName)) return false; if (this.ResourceId == null) diff --git a/src/dotnet/Common/Services/API/HttpClientFactoryService.cs b/src/dotnet/Common/Services/API/HttpClientFactoryService.cs index 5cebd98c68..7eadf2b9a0 100644 --- a/src/dotnet/Common/Services/API/HttpClientFactoryService.cs +++ b/src/dotnet/Common/Services/API/HttpClientFactoryService.cs @@ -195,7 +195,7 @@ private async Task GetEndpoint(string name, UnifiedUse { await EnsureConfigurationResourceProvider(); - var endpointConfiguration = await _configurationResourceProvider!.HandleGet( + var endpointConfiguration = await _configurationResourceProvider!.GetResourceAsync( $"/{ConfigurationResourceTypeNames.APIEndpointConfigurations}/{name}", userIdentity) ?? throw new Exception($"The resource provider {ResourceProviderNames.FoundationaLLM_Configuration} did not load the {name} endpoint configuration."); diff --git a/src/dotnet/Core/Services/CosmosDbService.cs b/src/dotnet/Common/Services/Azure/AzureCosmosDBService.cs similarity index 91% rename from src/dotnet/Core/Services/CosmosDbService.cs rename to src/dotnet/Common/Services/Azure/AzureCosmosDBService.cs index 7d9fce832c..13e5c100a1 100644 --- a/src/dotnet/Core/Services/CosmosDbService.cs +++ b/src/dotnet/Common/Services/Azure/AzureCosmosDBService.cs @@ -1,8 +1,8 @@ using FoundationaLLM.Common.Constants; -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Configuration.CosmosDB; using FoundationaLLM.Common.Models.Configuration.Users; -using FoundationaLLM.Core.Interfaces; +using FoundationaLLM.Common.Models.Conversation; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -10,12 +10,12 @@ using Polly.Retry; using System.Diagnostics; -namespace FoundationaLLM.Core.Services +namespace FoundationaLLM.Common.Services { /// /// Service to access Azure Cosmos DB for NoSQL. /// - public class CosmosDbService : ICosmosDbService + public class AzureCosmosDBService : ICosmosDBService { private Container _sessions; private Container _userSessions; @@ -30,19 +30,19 @@ public class CosmosDbService : ICosmosDbService private const string SoftDeleteQueryRestriction = " (not IS_DEFINED(c.deleted) OR c.deleted = false)"; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The settings retrieved /// by the injected . /// The Cosmos DB client. /// The logging interface used to log under the - /// type name. + /// type name. /// Thrown if any of the required settings /// are null or empty. - public CosmosDbService( + public AzureCosmosDBService( IOptions settings, CosmosClient client, - ILogger logger) + ILogger logger) { _settings = settings.Value; ArgumentException.ThrowIfNullOrEmpty(_settings.Endpoint); @@ -111,15 +111,15 @@ await _resiliencePipeline.ExecuteAsync(async token => await _database "/upn"), ThroughputProperties.CreateAutoscaleThroughput(1000), cancellationToken: token)!); /// - public async Task> GetSessionsAsync(string type, string upn, CancellationToken cancellationToken = default) + public async Task> GetSessionsAsync(string type, string upn, CancellationToken cancellationToken = default) { var query = new QueryDefinition($"SELECT DISTINCT * FROM c WHERE c.type = @type AND c.upn = @upn AND {SoftDeleteQueryRestriction} ORDER BY c._ts DESC") .WithParameter("@type", type) .WithParameter("@upn", upn); - var response = _userSessions.GetItemQueryIterator(query); + var response = _userSessions.GetItemQueryIterator(query); - List output = []; + List output = []; while (response.HasMoreResults) { var results = await response.ReadNextAsync(cancellationToken); @@ -130,9 +130,9 @@ public async Task> GetSessionsAsync(string type, string upn, Cance } /// - public async Task GetSessionAsync(string id, CancellationToken cancellationToken = default) + public async Task GetSessionAsync(string id, CancellationToken cancellationToken = default) { - var session = await _sessions.ReadItemAsync( + var session = await _sessions.ReadItemAsync( id: id, partitionKey: new PartitionKey(id), cancellationToken: cancellationToken); @@ -162,7 +162,7 @@ public async Task> GetSessionMessagesAsync(string sessionId, strin } /// - public async Task InsertSessionAsync(Session session, CancellationToken cancellationToken = default) + public async Task InsertSessionAsync(Conversation session, CancellationToken cancellationToken = default) { PartitionKey partitionKey = new(session.SessionId); return await _sessions.CreateItemAsync( @@ -211,7 +211,7 @@ public async Task UpdateMessageRatingAsync(string id, string sessionId, } /// - public async Task UpdateSessionAsync(Session session, CancellationToken cancellationToken = default) + public async Task UpdateSessionAsync(Conversation session, CancellationToken cancellationToken = default) { PartitionKey partitionKey = new(session.SessionId); return await _sessions.ReplaceItemAsync( @@ -223,9 +223,9 @@ public async Task UpdateSessionAsync(Session session, CancellationToken } /// - public async Task UpdateSessionNameAsync(string id, string sessionName, CancellationToken cancellationToken = default) + public async Task UpdateSessionNameAsync(string id, string sessionName, CancellationToken cancellationToken = default) { - var response = await _sessions.PatchItemAsync( + var response = await _sessions.PatchItemAsync( id: id, partitionKey: new PartitionKey(id), patchOperations: new[] @@ -258,7 +258,7 @@ public async Task UpsertSessionBatchAsync(params dynamic[] messages) } /// - public async Task UpsertUserSessionAsync(Session session, CancellationToken cancellationToken = default) + public async Task UpsertUserSessionAsync(Conversation session, CancellationToken cancellationToken = default) { PartitionKey partitionKey = new(session.UPN); await _userSessions.UpsertItemAsync( diff --git a/src/dotnet/Common/Services/DependencyInjection.cs b/src/dotnet/Common/Services/DependencyInjection.cs index 60a7dbcd4f..2e32aa0653 100644 --- a/src/dotnet/Common/Services/DependencyInjection.cs +++ b/src/dotnet/Common/Services/DependencyInjection.cs @@ -1,19 +1,25 @@ using Azure.Monitor.OpenTelemetry.AspNetCore; using FoundationaLLM.Common.Authentication; using FoundationaLLM.Common.Constants; +using FoundationaLLM.Common.Constants.Configuration; using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models.Configuration.CosmosDB; using FoundationaLLM.Common.Services; using FoundationaLLM.Common.Services.API; using FoundationaLLM.Common.Services.Azure; using FoundationaLLM.Common.Services.Security; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Azure.Cosmos.Fluent; +using Microsoft.Azure.Cosmos; using Microsoft.Extensions.Azure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Identity.Web; using OpenTelemetry.Resources; using OpenTelemetry.Trace; +using Microsoft.Extensions.Configuration; namespace FoundationaLLM { @@ -23,9 +29,9 @@ namespace FoundationaLLM public static partial class DependencyInjection { /// - /// Add CORS policies the the dependency injection container. + /// Adds CORS policies the the dependency injection container. /// - /// The host application builder. + /// The application builder managing the dependency injection container. public static void AddCorsPolicies(this IHostApplicationBuilder builder) => builder.Services.AddCors(policyBuilder => { @@ -41,9 +47,9 @@ public static void AddCorsPolicies(this IHostApplicationBuilder builder) => }); /// - /// Add OpenTelemetry the the dependency injection container. + /// Adds OpenTelemetry the the dependency injection container. /// - /// The host application builder. + /// The application builder managing the dependency injection container. /// The configuration key for the OpenTelemetry connection string. /// The name of the service. public static void AddOpenTelemetry(this IHostApplicationBuilder builder, @@ -70,9 +76,9 @@ public static void AddOpenTelemetry(this IHostApplicationBuilder builder, } /// - /// Add authentication configuration to the dependency injection container. + /// Adds authentication configuration to the dependency injection container. /// - /// The host application builder. + /// The application builder managing the dependency injection container. /// The configuration key for the Entra ID instance. /// The configuration key for the Entra ID tenant id. /// The configuration key for the Entra ID client id. @@ -124,7 +130,7 @@ public static void AddAuthenticationConfiguration(this IHostApplicationBuilder b } /// - /// Register the with the dependency injection container. + /// Registers the with the dependency injection container. /// /// The host application builder. /// The name of the configuration key that provides the URI of the Azure Key Vault service. @@ -145,9 +151,9 @@ public static void AddAzureKeyVaultService(this IHostApplicationBuilder builder, } /// - /// Register the with the dependency injection container. + /// Registers the with the dependency injection container. /// - /// The host application builder. + /// The application builder managing the dependency injection container. public static void AddHttpClientFactoryService(this IHostApplicationBuilder builder) { builder.Services.AddHttpClient(); @@ -156,7 +162,7 @@ public static void AddHttpClientFactoryService(this IHostApplicationBuilder buil } /// - /// Register the with the dependency injection container. + /// Registers the with the dependency injection container. /// /// The dependency injection container service collection. public static void AddHttpClientFactoryService(this IServiceCollection services) @@ -167,7 +173,7 @@ public static void AddHttpClientFactoryService(this IServiceCollection services) } /// - /// Register the implementation for a named API service with the dependency injection container. + /// Registers the implementation for a named API service with the dependency injection container. /// /// The host application builder. /// The name of the API service whose implementation is being registered. @@ -181,19 +187,51 @@ public static void AddDownstreamAPIService(this IHostApplicationBuilder builder, )); /// - /// Register the implementation with the dependency injection container. + /// Registers the implementation with the dependency injection container. /// - /// The host application builder. + /// The application builder managing the dependency injection container. public static void AddAzureResourceManager( this IHostApplicationBuilder builder) => builder.Services.AddSingleton(); /// - /// Register the implementation with the dependency injection container. + /// Registers the implementation with the dependency injection container. /// /// The dependency injection container service collection. public static void AddAzureResourceManager( this IServiceCollection services) => services.AddSingleton(); + + /// + /// Registers the implementation with the dependency injection container. + /// + /// The application builder managing the dependency injection container. + public static void AddAzureCosmosDBService(this IHostApplicationBuilder builder) => + builder.Services.AddAzureCosmosDBService(builder.Configuration); + + /// + /// Registers the implementation with the dependency injection container. + /// + /// The dependency injection container service collection. + /// The application configuration manager. + public static void AddAzureCosmosDBService(this IServiceCollection services, IConfigurationManager configuration) + { + services.AddOptions() + .Bind(configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_APIEndpoints_CoreAPI_Configuration_CosmosDB)); + + services.AddSingleton(serviceProvider => + { + var settings = serviceProvider.GetRequiredService>().Value; + return new CosmosClientBuilder(settings.Endpoint, DefaultAuthentication.AzureCredential) + .WithSerializerOptions(new CosmosSerializationOptions + { + PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase + }) + .WithConnectionModeGateway() + .Build(); + }); + + services.AddSingleton(); + } } } diff --git a/src/dotnet/Common/Services/Events/AzureEventGridEventService.cs b/src/dotnet/Common/Services/Events/AzureEventGridEventService.cs index 5e610cf1c2..020a203a90 100644 --- a/src/dotnet/Common/Services/Events/AzureEventGridEventService.cs +++ b/src/dotnet/Common/Services/Events/AzureEventGridEventService.cs @@ -55,6 +55,10 @@ public class AzureEventGridEventService : IEventService { EventSetEventNamespaces.FoundationaLLM_ResourceProvider_AzureOpenAI, null + }, + { + EventSetEventNamespaces.FoundationaLLM_ResourceProvider_Conversation, + null } }; diff --git a/src/dotnet/Common/Services/ResourceProviders/ResourceProviderResourceReferenceStore`1.cs b/src/dotnet/Common/Services/ResourceProviders/ResourceProviderResourceReferenceStore`1.cs index abbe9c56fe..b276bab21c 100644 --- a/src/dotnet/Common/Services/ResourceProviders/ResourceProviderResourceReferenceStore`1.cs +++ b/src/dotnet/Common/Services/ResourceProviders/ResourceProviderResourceReferenceStore`1.cs @@ -20,7 +20,8 @@ public class ResourceProviderResourceReferenceStore( IResourceProviderService resourceProvider, IStorageService resourceProviderStorageService, ILogger logger, - CancellationToken cancellationToken = default)where T : ResourceReference + CancellationToken cancellationToken = default) + where T : ResourceReference { private readonly IResourceProviderService _resourceProvider = resourceProvider; private readonly IStorageService _storage = resourceProviderStorageService; @@ -33,6 +34,12 @@ public class ResourceProviderResourceReferenceStore( private string ResourceReferencesFilePath => $"/{_resourceProvider.Name}/{RESOURCE_REFERENCES_FILE_NAME}"; private SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + private string? _defaultResourceName; + + /// + /// Gets the name of the default resource (if any). + /// + public string? DefaultResourceName => _defaultResourceName; /// /// Loads the resource references from the storage service. @@ -109,6 +116,56 @@ public async Task LoadResourceReferences() } } + /// + /// Attempts to get a resource reference by the unique name of the resource. + /// + /// The name of the resource. + /// + /// A tuple containing a boolean value (Success) indicating whether the resource reference was successfully retrieved, + /// a boolean value (Deleted) indicating whether the resource is deleted (and not purged), and the resource reference itself (ResourceReference). + /// + /// + /// + /// When Success is false, the ResourceReference will be null. + /// This means that the resource reference was not found in the store. + /// + /// + /// When Success is true, the ResourceReference will contain the reference to the resource. + /// This means that the resource reference is either valid or it has been logically deleted. + /// Callers should check the Deleted value to determine whether the resource was logically deleted. + /// + /// + public async Task<(bool Success, bool Deleted, T? ResourceReference)> TryGetResourceReference(string resourceName) + { + await _lock.WaitAsync(); + try + { + var success = TryGetResourceReferenceInternal(resourceName, out var deleted, out var resourceReference); + + if (success) + return (true, deleted, resourceReference); + + // The reference was not found which means it either does not exist or has been created by another instance of the resource provider. + + // Wait for 100 miliseconds to ensure that potential reference creation processes happening in different instances of the resource provider have completed. + await Task.Delay(100, _cancellationToken); + + await LoadAndMergeResourceReferences(); + + + // Try getting the reference again. + success = TryGetResourceReferenceInternal(resourceName, out deleted, out resourceReference); + + // Return the result, regardless of whether it is null or not. + // If it is null, the caller will have to handle the situation. + return (success, deleted, resourceReference); + } + finally + { + _lock.Release(); + } + } + /// /// Filters the resource references in the store based on the predicate. /// @@ -145,9 +202,10 @@ public async Task> GetResourceReferences(IEnumerable reso // Some of the resource references are missing, so we need to load them. await LoadAndMergeResourceReferences(); } + return resourceNames .Select(rn => _resourceReferences.GetValueOrDefault(rn)) - .Where(rr => rr is {Deleted: false}); + .Where(rr => (rr != null) && !rr.Deleted)!; } finally { @@ -162,7 +220,7 @@ public async Task> GetResourceReferences(IEnumerable reso /// /// This method is not safe in scenarios where multiple instances of a resource provider are running at the same time. /// - public async Task> GetAllResourceReferences() + public async Task> GetAllResourceReferences() { await _lock.WaitAsync(); try @@ -245,10 +303,12 @@ private async Task LoadAndMergeResourceReferences() _resourceProvider.StorageContainerName, ResourceReferencesFilePath, _cancellationToken); - var _persistedReferences = JsonSerializer.Deserialize>( - Encoding.UTF8.GetString(fileContent.ToArray()))!.ResourceReferences; + var persistedReferencesList = JsonSerializer.Deserialize>( + Encoding.UTF8.GetString(fileContent.ToArray()))!; + + _defaultResourceName = persistedReferencesList.DefaultResourceName; - foreach (var reference in _persistedReferences) + foreach (var reference in persistedReferencesList.ResourceReferences) { if (!_resourceReferences.ContainsKey(reference.Name)) { @@ -278,6 +338,37 @@ public async Task DeleteResourceReference(T resourceReference) } } + /// + /// + /// + /// + /// + /// + public async Task PurgeResourceReference(T resourceReference) + { + await _lock.WaitAsync(); + try + { + if (!resourceReference.Deleted) + throw new ResourceProviderException( + $"The resource reference for the resource {resourceReference.Name} cannot be purged. " + + "It is not marked as deleted.", + StatusCodes.Status400BadRequest); + + if (!_resourceReferences.Remove(resourceReference.Name)) + _logger.LogWarning( + "The resource reference for the resource {ResourceName} could not be purged. " + + "It was not found in the resource references store.", + resourceReference.Name); + else + await SaveResourceReferences(); + } + finally + { + _lock.Release(); + } + } + /// /// Saves the resource references to the storage service. /// @@ -326,5 +417,39 @@ await _storage.WriteFileAsync( else return null; } + + /// + /// Attempts to get a resource reference by the unique name of the resource. + /// + /// The name of the resource. + /// Indicates whether the resource was deleted and not purged (in this case the value is true). + /// The resource reference that matches the resource name. + /// True if the resource reference was successfully retrieved. + /// + /// If the resource exists and it was deleted without being also purged, the result will be false and output null. + /// + /// IMPORTANT! + /// Never call this method without acquiring the lock first. + /// + /// + private bool TryGetResourceReferenceInternal(string resourceName, out bool deleted, out T? resourceReference) + { + resourceReference = null; + deleted = false; + + if (_resourceReferences.TryGetValue(resourceName, out var reference)) + { + if (reference == null) + return false; + + if (reference.Deleted) + deleted = true; + + resourceReference = reference; + return true; + } + else + return false; + } } } diff --git a/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs b/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs index 74c1ce88d9..e1a9df53c7 100644 --- a/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs +++ b/src/dotnet/Common/Services/ResourceProviders/ResourceProviderServiceBase.cs @@ -34,7 +34,7 @@ public class ResourceProviderServiceBase : IResourceProvider private readonly Dictionary _allowedResourceTypes; private readonly Dictionary _resourceProviders = []; - private readonly bool _useInternalStore; + private readonly bool _useInternalReferencesStore; private readonly SemaphoreSlim _lock = new(1, 1); /// @@ -121,7 +121,7 @@ public class ResourceProviderServiceBase : IResourceProvider /// The logger used for logging. /// The of the main dependency injection container. /// The list of Event Service event namespaces to subscribe to for local event processing. - /// Indicates whether the resource provider should use the internal resource store or provide one of its own. + /// Indicates whether the resource provider should use the internal resource references store or provide one of its own. public ResourceProviderServiceBase( InstanceSettings instanceSettings, IAuthorizationService authorizationService, @@ -131,7 +131,7 @@ public ResourceProviderServiceBase( IServiceProvider serviceProvider, ILogger logger, List? eventNamespacesToSubscribe = default, - bool useInternalStore = false) + bool useInternalReferencesStore = false) { _authorizationService = authorizationService; _storageService = storageService; @@ -141,7 +141,7 @@ public ResourceProviderServiceBase( _serviceProvider = serviceProvider; _instanceSettings = instanceSettings; _eventNamespacesToSubscribe = eventNamespacesToSubscribe; - _useInternalStore = useInternalStore; + _useInternalReferencesStore = useInternalReferencesStore; _allowedResourceProviders = [_name]; _allowedResourceTypes = GetResourceTypes(); @@ -160,9 +160,9 @@ public async Task Initialize() { _logger.LogInformation("Starting to initialize the {ResourceProvider} resource provider...", _name); - //TODO: Remove this check after all resource providers are updated to use the new resource reference store. - if (_useInternalStore) + if (_useInternalReferencesStore) { + // The resource provider uses the default internal resource reference store. _resourceReferenceStore = new ResourceProviderResourceReferenceStore( this, _storageService, @@ -194,6 +194,22 @@ public async Task Initialize() } } + /// + public async Task WaitForInitialization() + { + if (IsInitialized) + return; + + for (int i = 0; i < 6; i++) + { + await Task.Delay(TimeSpan.FromSeconds(10)); + if (IsInitialized) + return; + } + + throw new ResourceProviderException($"The resource provider {Name} did not initialize within the expected time frame."); + } + #region Virtuals to override in derived classes /// @@ -219,97 +235,100 @@ protected virtual async Task InitializeInternal() #region IManagementProviderService /// - public async Task HandleGetAsync(string resourcePath, UnifiedUserIdentity userIdentity) + public async Task HandleGetAsync(string resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) { EnsureServiceInitialization(); - var parsedResourcePath = EnsureValidResourcePath(resourcePath, HttpMethod.Get, false); + var (ParsedResourcePath, AuthorizableOperation) = ParseAndValidateResourcePath(resourcePath, HttpMethod.Get, false, requireResource: false); - if (!parsedResourcePath.IsResourceTypePath) - { - // Authorize access to the resource path. - await Authorize(parsedResourcePath, userIdentity, "read"); - } + // Authorize access to the resource path. + var authorizationResult = ParsedResourcePath.IsResourceTypePath + ? await Authorize(ParsedResourcePath, userIdentity, AuthorizableOperation, true, options?.IncludeRoles ?? false) + : await Authorize(ParsedResourcePath, userIdentity, AuthorizableOperation, false, false); - return await GetResourcesAsync(parsedResourcePath, userIdentity); + return await GetResourcesAsync(ParsedResourcePath, authorizationResult, userIdentity); } /// public async Task HandlePostAsync(string resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) { EnsureServiceInitialization(); - var parsedResourcePath = EnsureValidResourcePath(resourcePath, HttpMethod.Post, true); - - // Authorize access to the resource path. - await Authorize(parsedResourcePath, userIdentity, "write"); + var (ParsedResourcePath, AuthorizableOperation) = ParseAndValidateResourcePath(resourcePath, HttpMethod.Post, true, requireResource: false); - if (parsedResourcePath.ResourceTypeInstances.Last().Action != null) - return await ExecuteActionAsync(parsedResourcePath, serializedResource, userIdentity); - else + if (ParsedResourcePath.HasAction) { - var resource = await UpsertResourceAsync(parsedResourcePath, serializedResource, userIdentity); + // Handle the action. - var upsertResult = resource as ResourceProviderUpsertResult; + // Some actions require a resource identifier. + if (ParsedResourcePath.Action! == ResourceProviderActions.Purge + && !ParsedResourcePath.HasResourceId) + throw new ResourceProviderException( + $"The resource path {resourcePath} is required to have a resource identifier but none was found.", + StatusCodes.Status400BadRequest); - if (upsertResult!.ResourceExists == false && Name != ResourceProviderNames.FoundationaLLM_Authorization) - { - var roleAssignmentName = Guid.NewGuid().ToString(); - var roleAssignmentDescription = $"Owner role for {userIdentity.Name}"; - var roleAssignmentResult = await _authorizationService.ProcessRoleAssignmentRequest( - _instanceSettings.Id, - new RoleAssignmentRequest() - { - Name = roleAssignmentName, - Description = roleAssignmentDescription, - ObjectId = $"/instances/{_instanceSettings.Id}/providers/{ResourceProviderNames.FoundationaLLM_Authorization}/{AuthorizationResourceTypeNames.RoleAssignments}/{roleAssignmentName}", - PrincipalId = userIdentity.UserId!, - PrincipalType = PrincipalTypes.User, - RoleDefinitionId = $"/providers/{ResourceProviderNames.FoundationaLLM_Authorization}/{AuthorizationResourceTypeNames.RoleDefinitions}/{RoleDefinitionNames.Owner}", - Scope = upsertResult!.ObjectId ?? throw new ResourceProviderException($"The {roleAssignmentDescription} could not be assigned. Could not set the scope for the resource.") - }, - userIdentity); - - if (!roleAssignmentResult.Success) - _logger.LogError("The {RoleAssignment} could not be assigned.", roleAssignmentDescription); - } + // Authorize access to the resource path. + // In the special case of the filter action, if the resource type path is not directly authorized, + // the subordinate authorized resource paths must be expanded (and the overrides for ExecuteActionAsync must handle this). + var actionAuthorizationResult = await Authorize(ParsedResourcePath, userIdentity, AuthorizableOperation, + ParsedResourcePath.Action! == ResourceProviderActions.Filter, false); - return resource; + return await ExecuteActionAsync(ParsedResourcePath, actionAuthorizationResult, serializedResource, userIdentity); } + + // All resource upserts require a resource identifier. + if (!ParsedResourcePath.HasResourceId) + throw new ResourceProviderException( + $"The resource path {resourcePath} is required to have a resource identifier but none was found.", + StatusCodes.Status400BadRequest); + + // Authorize access to the resource path. + var authorizationResult = await Authorize(ParsedResourcePath, userIdentity, AuthorizableOperation, false, false); + + var upsertResult = await UpsertResourceAsync(ParsedResourcePath, serializedResource, userIdentity); + + await UpsertResourcePostProcess(ParsedResourcePath.InstanceId!, (upsertResult as ResourceProviderUpsertResult)!, authorizationResult, userIdentity); + + return upsertResult; } /// public async Task HandleDeleteAsync(string resourcePath, UnifiedUserIdentity userIdentity) { EnsureServiceInitialization(); - var parsedResourcePath = EnsureValidResourcePath(resourcePath, HttpMethod.Delete, false); + var (ParsedResourcePath, AuthorizableOperation) = ParseAndValidateResourcePath(resourcePath, HttpMethod.Delete, false); // Authorize access to the resource path. - await Authorize(parsedResourcePath, userIdentity, "delete"); + await Authorize(ParsedResourcePath, userIdentity, AuthorizableOperation, false, false); - await DeleteResourceAsync(parsedResourcePath, userIdentity); + await DeleteResourceAsync(ParsedResourcePath, userIdentity); } - /// - /// Gets a object for the specified string resource path. - /// - /// The resource path. - /// Indicates whether actions are allowed in the resource path. - /// A object. - public ResourcePath GetResourcePath(string resourcePath, bool allowAction = true) => - new( - resourcePath, - _allowedResourceProviders, - _allowedResourceTypes, - allowAction: allowAction); - #region Virtuals to override in derived classes /// /// The internal implementation of GetResourcesAsync. Must be overridden in derived classes. /// /// A containing information about the resource path. + /// The containing the result of the resource path authorization request. /// The with details about the identity of the user. + /// The which provides operation parameters. /// - protected virtual async Task GetResourcesAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) + /// + /// The override implementation should return a list of resources or a single resource, depending on the resource path. + /// It also must handle the authorization result and return the appropriate response as follows: + /// + /// The resource path refers to a single resource. In this case, the authorization is already confirmed and + /// the specific resource should be returned. + /// The resource path refers to a resource type and the read action is authorized for the resource path itself. + /// In this case, all resources must be returned according to the PBAC policies specified by the authorization result (if any). + /// The resource path refers to a resource type and the read action is denied for the resource path itself. + /// In this case, only the resources specified in the subordinate authorized resource paths list of the authorization result should be returned (if any). + /// + /// + protected virtual async Task GetResourcesAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity, + ResourceProviderLoadOptions? options = null) { await Task.CompletedTask; throw new NotImplementedException(); @@ -332,11 +351,27 @@ protected virtual async Task UpsertResourceAsync(ResourcePath resourcePa /// The internal implementation of ExecuteActionAsync. Must be overriden in derived classes. /// /// A containing information about the resource path. + /// The containing the result + /// of the resource path authorization request. /// The serialized details of the action being executed. /// The with details about the identity of the user. /// - /// - protected virtual async Task ExecuteActionAsync(ResourcePath resourcePath, string serializedAction, UnifiedUserIdentity userIdentity) + /// + /// In the special case of the filter action, the override must handle the authorization result and return + /// the appropriate response as follows: + /// + /// The read action is authorized for the resource path itself. + /// In this case, all matching resources must be returned according to the PBAC policies specified by the authorization result (if any). + /// The read action is denied for the resource path itself. + /// In this case, only the matching resources specified in the subordinate authorized resource paths list + /// of the authorization result should be returned (if any). + /// + /// + protected virtual async Task ExecuteActionAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + string serializedAction, + UnifiedUserIdentity userIdentity) { await Task.CompletedTask; throw new NotImplementedException(); @@ -361,55 +396,86 @@ protected virtual async Task DeleteResourceAsync(ResourcePath resourcePath, Unif #region IResourceProviderService /// - public async Task GetResource(string resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderOptions? options = null) where T : class + public async Task>> GetResourcesAsync(string instanceId, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) + where T : ResourceBase + { + EnsureServiceInitialization(); + var (ParsedResourcePath, AuthorizableOperation) = + CreateAndValidateResourcePath(instanceId, HttpMethod.Get, typeof(T)); + + var authorizationResult = + await Authorize(ParsedResourcePath, userIdentity, AuthorizableOperation, true, options?.IncludeRoles ?? false); + + return ((await GetResourcesAsync(ParsedResourcePath, authorizationResult, userIdentity)) as List>)!; + } + + /// + public async Task GetResourceAsync(string resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) + where T : ResourceBase + { + EnsureServiceInitialization(); + var (ParsedResourcePath, AuthorizableOperation) = + ParseAndValidateResourcePath(resourcePath, HttpMethod.Get, false, typeof(T)); + + // Authorize access to the resource path. + await Authorize(ParsedResourcePath, userIdentity, AuthorizableOperation, false, false); + + return await GetResourceAsyncInternal(ParsedResourcePath, userIdentity, options); + } + + /// + public async Task GetResourceAsync(string instanceId, string resourceName, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) + where T : ResourceBase { EnsureServiceInitialization(); - var parsedResourcePath = EnsureValidResourcePath(resourcePath, HttpMethod.Get, false, typeof(T)); + var (ParsedResourcePath, AuthorizableOperation) = + CreateAndValidateResourcePath(instanceId, HttpMethod.Get, typeof(T), resourceName); // Authorize access to the resource path. - await Authorize(parsedResourcePath, userIdentity, "read"); + await Authorize(ParsedResourcePath, userIdentity, AuthorizableOperation, false, false); - return await GetResourceInternal(parsedResourcePath, userIdentity, options); + return await GetResourceAsyncInternal(ParsedResourcePath, userIdentity, options); } /// - public async Task UpsertResourceAsync(string resourcePath, T resource, UnifiedUserIdentity userIdentity) + public async Task UpsertResourceAsync(string instanceId, T resource, UnifiedUserIdentity userIdentity) where T : ResourceBase where TResult : ResourceProviderUpsertResult { EnsureServiceInitialization(); - var parsedResourcePath = EnsureValidResourcePath(resourcePath, HttpMethod.Post, false, typeof(T)); + var (ParsedResourcePath, AuthorizableOperation) = CreateAndValidateResourcePath(instanceId, HttpMethod.Post, typeof(T), resourceName: resource.Name); // Authorize access to the resource path. - await Authorize(parsedResourcePath, userIdentity, "write"); + var authorizationResult = await Authorize(ParsedResourcePath, userIdentity, AuthorizableOperation, false, false); - var result = await UpsertResourceAsyncInternal(parsedResourcePath, resource, userIdentity); + var upsertResult = await UpsertResourceAsyncInternal(ParsedResourcePath, resource, userIdentity); - var upsertResult = result as ResourceProviderUpsertResult; + await UpsertResourcePostProcess(ParsedResourcePath.InstanceId!, upsertResult, authorizationResult, userIdentity); - if (upsertResult!.ResourceExists == false && Name != ResourceProviderNames.FoundationaLLM_Authorization) - { - var roleAssignmentName = Guid.NewGuid().ToString(); - var roleAssignmentDescription = $"Owner role for {userIdentity.Name}"; - var roleAssignmentResult = await _authorizationService.ProcessRoleAssignmentRequest( - _instanceSettings.Id, - new RoleAssignmentRequest() - { - Name = roleAssignmentName, - Description = roleAssignmentDescription, - ObjectId = $"/instances/{_instanceSettings.Id}/providers/{ResourceProviderNames.FoundationaLLM_Authorization}/{AuthorizationResourceTypeNames.RoleAssignments}/{roleAssignmentName}", - PrincipalId = userIdentity.UserId!, - PrincipalType = PrincipalTypes.User, - RoleDefinitionId = $"/providers/{ResourceProviderNames.FoundationaLLM_Authorization}/{AuthorizationResourceTypeNames.RoleDefinitions}/{RoleDefinitionNames.Owner}", - Scope = upsertResult!.ObjectId ?? throw new ResourceProviderException($"The {roleAssignmentDescription} could not be assigned. Could not set the scope for the resource.") - }, - userIdentity); + return upsertResult; + } - if (!roleAssignmentResult.Success) - _logger.LogError("The {RoleAssignment} could not be assigned.", roleAssignmentDescription); - } + /// + public async Task<(bool Exists, bool Deleted)> ResourceExists(string instanceId, string resourceName, UnifiedUserIdentity userIdentity) + where T : ResourceBase + { + EnsureServiceInitialization(); + var (ParsedResourcePath, AuthorizableOperation) = + CreateAndValidateResourcePath(instanceId, HttpMethod.Get, typeof(T), resourceName: resourceName); - return result; + // Authorize access to the resource path. + await Authorize(ParsedResourcePath, userIdentity, AuthorizableOperation, false, false); + + var resourceNameCheckResult = await CheckResourceName(new ResourceName + { + Name= resourceName + }); + + return + ( + resourceNameCheckResult.Exists, + resourceNameCheckResult.Deleted + ); } #region Virtuals to override in derived classes @@ -419,9 +485,10 @@ public async Task UpsertResourceAsync(string resourcePath, /// /// A containing information about the resource path. /// The providing information about the calling user identity. - /// The which provides operation parameters. + /// The which provides operation parameters. /// - protected virtual async Task GetResourceInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderOptions? options = null) where T : class + protected virtual async Task GetResourceAsyncInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) + where T : ResourceBase { await Task.CompletedTask; throw new NotImplementedException(); @@ -455,10 +522,13 @@ protected virtual async Task UpsertResourceAsyncInternal(Re /// /// /// The containing information about the identity of the user. - /// + /// The type of action to be authorized (e.g., "read", "write", "delete"). + /// Indicates whether to expand resource type paths that are not authorized. + /// Indicates whether to include roles in the response. /// /// - private async Task Authorize(ResourcePath resourcePath, UnifiedUserIdentity? userIdentity, string actionType) + private async Task Authorize(ResourcePath resourcePath, UnifiedUserIdentity? userIdentity, string actionType, + bool expandResourceTypePaths, bool includeRoles) { try { @@ -469,12 +539,25 @@ private async Task Authorize(ResourcePath resourcePath, UnifiedUserIdentity? use var rp = resourcePath.GetObjectId(_instanceSettings.Id, _name); var result = await _authorizationService.ProcessAuthorizationRequest( _instanceSettings.Id, - $"{_name}/{resourcePath.MainResourceType!}/{actionType}", + $"{_name}/{resourcePath.MainResourceTypeName!}/{actionType}", [rp], + expandResourceTypePaths, + includeRoles, userIdentity); - if (!result.AuthorizationResults[rp]) + if (!result.AuthorizationResults[rp].Authorized + && !resourcePath.IsResourceTypePath) + { + // Only throw an exception if the resource path refers to a specific resource. + // For a resource path that refers to a resource type, it is acceptable to not be authorized directly. + // When this happens, one of the following will occur: + // 1. The expandResourceTypePaths parameter is set to true, in which case the response will include + // any authorized subordinate resource paths (if there are none, the response will be empty). + // 2. The expandResourceTypePaths parameter is set to false, in which case the response will be empty. throw new AuthorizationException("Access is not authorized."); + } + + return result.AuthorizationResults[rp]; } catch (AuthorizationException) { @@ -513,34 +596,101 @@ private void EnsureServiceInitialization() throw new ResourceProviderException($"The resource provider {_name} is not initialized."); } - private ResourcePath EnsureValidResourcePath(string resourcePath, HttpMethod operationType, bool allowAction = true, Type? resourceType = null) + private (ResourcePath ParsedResourcePath, string AuthorizableOperation) CreateAndValidateResourcePath( + string instanceId, + HttpMethod operationType, + Type resourceType, + string? resourceName = null) { + var result = GetResourcePath(instanceId, resourceType, resourceName); var parsedResourcePath = new ResourcePath( - resourcePath, + result.ResourcePath, _allowedResourceProviders, _allowedResourceTypes, - allowAction: allowAction); + allowAction: false); - var mainResourceType = parsedResourcePath.MainResourceType + var resourceAllowedTypes = + result.ResourceTypeDescriptor.AllowedTypes.SingleOrDefault(at => at.HttpMethod == operationType.Method) ?? throw new ResourceProviderException( - $"The resource path {resourcePath} does not have a main resource type and cannot be handled by the {_name} resource provider.", + $"The HTTP method {operationType.Method} is not supported for resources of type {resourceType.Name} by the {_name} resource provider.", StatusCodes.Status400BadRequest); - if (!AllowedResourceTypes.TryGetValue(mainResourceType, out ResourceTypeDescriptor? resourceTypeDescriptor)) + return + ( + parsedResourcePath, + resourceAllowedTypes.AuthorizableOperation + ); + } + + private (ResourcePath ParsedResourcePath, string AuthorizableOperation) ParseAndValidateResourcePath( + string resourcePath, + HttpMethod operationType, + bool allowAction = true, + Type? resourceType = null, + bool requireResource = true) + { + var parsedResourcePath = new ResourcePath( + resourcePath, + _allowedResourceProviders, + _allowedResourceTypes, + allowAction: allowAction); + + if (parsedResourcePath.ResourceTypeInstances.Count == 0) throw new ResourceProviderException( - $"The resource type {mainResourceType} cannot be handled by the {_name} resource provider", + $"The resource path {resourcePath} does not have any resource type instances and cannot be handled by the {_name} resource provider.", StatusCodes.Status400BadRequest); - if (operationType.Method == HttpMethods.Post) + ResourceTypeDescriptor? currentResourceTypeDescriptor = null; + var currentAllowedResourceTypes = _allowedResourceTypes; + + foreach (var resourceTypeInstance in parsedResourcePath.ResourceTypeInstances) { - if (resourceType != null - && !resourceTypeDescriptor.AllowedTypes.Single(at => at.HttpMethod == operationType.Method).AllowedBodyTypes.Contains(resourceType)) + if (currentAllowedResourceTypes == null + || !currentAllowedResourceTypes.TryGetValue(resourceTypeInstance.ResourceTypeName, out currentResourceTypeDescriptor)) throw new ResourceProviderException( - $"The type {nameof(resourceType)} is not supported by the {_name} resource provider.", + $"The resource type {resourceTypeInstance.ResourceTypeName} cannot be handled by the {_name} resource provider", StatusCodes.Status400BadRequest); + currentAllowedResourceTypes = currentResourceTypeDescriptor.SubTypes; } - return parsedResourcePath; + if (requireResource + && !parsedResourcePath.HasResourceId) + throw new ResourceProviderException( + $"The resource path {resourcePath} is required to have a resource identifier but none was found.", + StatusCodes.Status400BadRequest); + + if (!allowAction + && parsedResourcePath.HasAction) + throw new ResourceProviderException( + $"The resource path {resourcePath} is not allowed to have an action.", + StatusCodes.Status400BadRequest); + + if (resourceType != null + && currentResourceTypeDescriptor!.ResourceType != resourceType) + throw new ResourceProviderException( + $"The resource type {resourceType.Name} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest); + + if (parsedResourcePath.HasAction) + { + var allowedTypes = currentResourceTypeDescriptor!.Actions? + .SingleOrDefault(a => a.Name == parsedResourcePath.Action)? + .AllowedTypes? + .SingleOrDefault(at => at.HttpMethod == operationType.Method) + ?? throw new ResourceProviderException( + $"The resource path {resourcePath} does not support operation {operationType.Method}.", + StatusCodes.Status400BadRequest); + return (parsedResourcePath, allowedTypes.AuthorizableOperation); + } + else + { + var allowedTypes = currentResourceTypeDescriptor!.AllowedTypes? + .SingleOrDefault(at => at.HttpMethod == operationType.Method) + ?? throw new ResourceProviderException( + $"The resource path {resourcePath} does not support operation {operationType.Method}.", + StatusCodes.Status400BadRequest); + return (parsedResourcePath, allowedTypes.AuthorizableOperation); + } } #endregion @@ -552,56 +702,93 @@ private ResourcePath EnsureValidResourcePath(string resourcePath, HttpMethod ope /// /// The type of resources to load. /// The that indicates a specific resource to load. + /// The containing the result of the resource path authorization request. + /// The which provides operation parameters. + /// An optional function that loads the resource used to override + /// the default resource loading mechanism. /// A list of objects. - protected async Task>> LoadResources(ResourceTypeInstance instance) where T : ResourceBase + protected async Task>> LoadResources( + ResourceTypeInstance instance, + ResourcePathAuthorizationResult authorizationResult, + ResourceProviderLoadOptions? options = null, + Func>? customResourceLoader = null) where T : ResourceBase { + Func> resourceLoader = + customResourceLoader == null + ? async (resourceReference) => + (await LoadResource(resourceReference))! + : async (resourceReference) => + (await customResourceLoader(resourceReference, options?.LoadContent ?? false))!; + + IEnumerable resourceReferencesToLoad = []; + + // Keep the lock for the shortest possible time (until compiling the list of resource references to load). + try { await _lock.WaitAsync(); - if (instance.ResourceId == null) + if (instance.ResourceId != null) { - var allResourceReferences = - await _resourceReferenceStore!.GetAllResourceReferences(); - var resources = (await Task.WhenAll( - allResourceReferences - .Select(r => LoadResource(r)))) - .Where(r => r != null) - .ToList(); - - return resources.Select(r => new ResourceProviderGetResult() - { - Resource = r!, - Actions = [], - Roles = [] - }).ToList(); + // Loading a specific resource. + // No need to check the authorization result here, as it has already been checked by the Authorize method. + // The Authorize method is the one that produces the authorization result and it throws an exception if + // the authorization fails when loading a specific resource. + // See the comments inside the Authorize method for more details. + + var resourceReference = await _resourceReferenceStore!.GetResourceReference(instance.ResourceId) + ?? throw new ResourceProviderException( + $"The resource reference for resource {instance.ResourceId} could not be found.", + StatusCodes.Status404NotFound); + + resourceReferencesToLoad = [resourceReference]; } else { - var resourceReference = await _resourceReferenceStore!.GetResourceReference(instance.ResourceId); - - if (resourceReference != null) - { - var resource = await LoadResource(resourceReference); - return resource == null - ? [] - : [ - new ResourceProviderGetResult() - { - Resource = resource, - Actions = [], - Roles = [] - } - ]; - } - else - return []; + resourceReferencesToLoad = authorizationResult.Authorized + ? await _resourceReferenceStore!.GetAllResourceReferences() + : await _resourceReferenceStore!.GetResourceReferences( + authorizationResult.SubordinateResourcePathsAuthorizationResults.Values + .Where(sarp => !string.IsNullOrWhiteSpace(sarp.ResourceName)) + .Select(sarp => sarp.ResourceName!) + .ToList()); } + } finally { _lock.Release(); } + + // Proceed to load the resources + + if (instance.ResourceId != null) + { + // Handle the case of loading a specific resource separately. + // This is because we need to throw in case the resource cannot be loaded. + + var resource = await resourceLoader(resourceReferencesToLoad.First()); + if (resource != null) + { + return + [ + new ResourceProviderGetResult + { + Resource = resource, + Roles = (options?.IncludeRoles ?? false) + ? authorizationResult.Roles + : [] + } + ]; + } + else + throw new ResourceProviderException($"The resource {instance.ResourceId} could not be loaded.", + StatusCodes.Status500InternalServerError); + } + + // Loading multiple resources of a specific type according to the authorization result. + + return await LoadResourcesFromReferences(resourceReferencesToLoad, authorizationResult, resourceLoader, options); } /// @@ -611,9 +798,12 @@ protected async Task>> LoadResources(Resour /// The type of resource reference used to indetify the resource to load. /// The loaded resource. /// + /// + /// Always ensure this method is called within a lock to avoid unexpected racing conditions. + /// protected async Task LoadResource(TResourceReference resourceReference) where T : ResourceBase { - if (resourceReference.ResourceType != typeof(T)) + if (!typeof(T).IsAssignableFrom(resourceReference.ResourceType)) throw new ResourceProviderException( $"The resource reference {resourceReference.Name} is not of the expected type {typeof(T).Name}.", StatusCodes.Status400BadRequest); @@ -626,7 +816,7 @@ protected async Task>> LoadResources(Resour Encoding.UTF8.GetString(fileContent.ToArray()), _serializerSettings) ?? throw new ResourceProviderException($"Failed to load the resource {resourceReference.Name}. Its content file might be corrupt.", - StatusCodes.Status400BadRequest); + StatusCodes.Status500InternalServerError); return resourceObject; } @@ -820,41 +1010,268 @@ await _storageService.WriteFileAsync( } /// - /// Deletes a resource and its reference. + /// Deletes a resource. /// /// The type of resource to delete. - /// The name of the resource. + /// The identifying the resource to delete. /// /// - protected async Task DeleteResource(string resourceName) + /// + /// The operation is a logical delete. The resource reference is marked deleted, but the resource content remains in storage. + /// To fully remove a resource, the delete operation must be followed by a purge operation. + /// + protected async Task DeleteResource(ResourcePath resourcePath) { + var resourceName = resourcePath.ResourceId + ?? throw new ResourceProviderException("The specified path does not contain a resource identifier.", + StatusCodes.Status400BadRequest); + try { await _lock.WaitAsync(); - var resourceReference = await _resourceReferenceStore!.GetResourceReference(resourceName); + var result = await _resourceReferenceStore!.TryGetResourceReference(resourceName); + + if (result.Success + && !result.Deleted) + { + await _resourceReferenceStore!.DeleteResourceReference(result.ResourceReference!); + } + else + { + throw new ResourceProviderException($"The resource {resourceName} cannot be deleted because it was either already deleted or does not exist.", + StatusCodes.Status404NotFound); + } + } + finally + { + _lock.Release(); + } + } + + /// + /// Purges a deleted resource. + /// + /// The type of the resource to purge. + /// The identifying the resource to purge. + /// A indicating the outcome of the operation. + /// + /// + /// The operation can only be applied to a resource that has been logically deleted. + /// + protected async Task PurgeResource(ResourcePath resourcePath) + { + var resourceName = resourcePath.ResourceId + ?? throw new ResourceProviderException("The specified path does not contain a resource identifier.", + StatusCodes.Status400BadRequest); + + try + { + await _lock.WaitAsync(); - if (resourceReference != null) + var result = await _resourceReferenceStore!.TryGetResourceReference(resourceName); + if (result.Success && result.Deleted) { - await _resourceReferenceStore!.DeleteResourceReference(resourceReference); + // Conditions are met to purge the resource. + + // Delete the resource file from storage. await _storageService.DeleteFileAsync( _storageContainerName, - resourceReference.Filename); + result.ResourceReference!.Filename, + default); + + // Remove the resource reference from the store. + await _resourceReferenceStore!.PurgeResourceReference(result.ResourceReference!); + + return new ResourceProviderActionResult(true); } else { - throw new ResourceProviderException($"Could not locate the {resourceName} resource.", - StatusCodes.Status404NotFound); + throw new ResourceProviderException( + $"The resource {resourceName} cannot be purged because it is either not soft-deleted or does not exist.", + StatusCodes.Status400BadRequest); + } + } + finally + { + _lock.Release(); + } + } + + /// + /// Checks if a resource name is available. + /// + /// The type of resource for which the name check is performed. + /// The providing the name to be checked for availability. + /// A indicating the outcome of the operation. + protected async Task CheckResourceName(ResourceName resourceName) + { + var result = await _resourceReferenceStore!.TryGetResourceReference(resourceName.Name); + + // The name is denied if one of the following conditions is met: + // 1. A resource with the specified name already exists (hence the reference was successfully retrieved). + // 2. A resource with the specified name was previously deleted and not purged. + return result.Success + ? new ResourceNameCheckResult + { + Name = resourceName!.Name, + Type = resourceName.Type, + Status = NameCheckResultType.Denied, + Exists = result.Success, + Deleted = result.Deleted, + Message = "A resource with the specified name already exists or was previously deleted and not purged." } + : new ResourceNameCheckResult + { + Name = resourceName!.Name, + Type = resourceName.Type, + Status = NameCheckResultType.Allowed, + Exists = result.Success, + Deleted = result.Deleted + }; + } + + /// + /// Loads a list of resources filtered based on object IDs. + /// + /// The type of resources to load. + /// The resource type path to filter. + /// The used to filter the resources. + /// The containing the result of the resource path authorization request. + /// The which provides operation parameters. + /// An optional function that loads the resource used to override + /// the default resource loading mechanism. + /// A list of objects of type . + protected async Task> FilterResources( + ResourcePath resourcePath, + ResourceFilter filter, + ResourcePathAuthorizationResult authorizationResult, + ResourceProviderLoadOptions? options = null, + Func>? customResourceLoader = null) + where T : ResourceBase + { + if (!resourcePath.IsResourceTypePath) + throw new ResourceProviderException($"The resource path {resourcePath.RawResourcePath} is not a resource type path.", + StatusCodes.Status400BadRequest); + + Func> resourceLoader = + customResourceLoader == null + ? async (resourceReference) => + (await LoadResource(resourceReference))! + : async (resourceReference) => + (await customResourceLoader(resourceReference, options?.LoadContent ?? false))!; + + List filterResourceNames = []; + + if (filter.DefaultResource.HasValue + && filter.DefaultResource.Value) + { + // Load the default resource for the resource type path. + + if (string.IsNullOrEmpty(_resourceReferenceStore!.DefaultResourceName)) + throw new ResourceProviderException( + "The default resource name is not set for the resource provider.", + StatusCodes.Status500InternalServerError); + + filterResourceNames = [_resourceReferenceStore.DefaultResourceName]; + } + else + { + // Filter resources based on the object IDs provided in the filter. + + var filterResourcePaths = filter.ObjectIDs? + .Select(id => this.GetParsedResourcePath(id, false)) + .ToList() + ?? []; + + if (filterResourcePaths.Count == 0 + || filterResourcePaths.Any(rp => !rp.HasResourceId || !rp.MatchesResourceTypes(resourcePath))) + throw new ResourceProviderException( + "The list of filter object IDs is either empty or contains invalid values.", + StatusCodes.Status400BadRequest); + + filterResourceNames = filterResourcePaths + .Select(rp => rp.ResourceId!) + .ToList(); + } + + IEnumerable resourceReferencesToLoad = []; + + // Keep the lock for the shortest possible time (until compiling the list of resource references to load). + + try + { + await _lock.WaitAsync(); + + resourceReferencesToLoad = authorizationResult.Authorized + ? await _resourceReferenceStore!.GetResourceReferences(filterResourceNames) + : await _resourceReferenceStore!.GetResourceReferences( + authorizationResult.SubordinateResourcePathsAuthorizationResults.Values + .Where(sarp => !string.IsNullOrWhiteSpace(sarp.ResourceName) && filterResourceNames.Contains(sarp.ResourceName)) + .Select(sarp => sarp.ResourceName!) + .ToList()); } finally { _lock.Release(); } + + var result = await LoadResourcesFromReferences( + resourceReferencesToLoad, + authorizationResult, + async (resourceReference) => + (await resourceLoader(resourceReference))!, + options); + + return result.Select(r => r.Resource); + } + + #region Helpers + + private async Task>> LoadResourcesFromReferences( + IEnumerable resourceReferences, + ResourcePathAuthorizationResult authorizationResult, + Func> resourceLoader, + ResourceProviderLoadOptions? options = null) where T : ResourceBase + { + List> results = []; + + foreach (var resourceReference in resourceReferences) + { + // Attempt to identify the subordinate authorization result for the intermediate resource. + authorizationResult.SubordinateResourcePathsAuthorizationResults.TryGetValue( + resourceReference.Name, + out ResourcePathAuthorizationResult? subordinateAuthorizationResult); + + // An intermediate resource will be returned only if one of the following conditions is met: + // 1. The resource type path itself is authorized. + // 2. The resource type path itself is not authorized, but the intermediate resource exists in the + // subordinate resource paths authorization results and is authorized. + if (authorizationResult.Authorized + || (subordinateAuthorizationResult?.Authorized ?? false)) + { + var loadedResource = await resourceLoader(resourceReference); + if (loadedResource != null) + results.Add( + new ResourceProviderGetResult + { + Resource = loadedResource, + Roles = (options?.IncludeRoles ?? false) + ? authorizationResult.Roles + .Union(subordinateAuthorizationResult?.Roles ?? []) + .ToList() + : [] + }); + } + } + + return results; } #endregion + #endregion + #region Utils /// @@ -894,6 +1311,90 @@ protected void UpdateBaseProperties(ResourceBase resource, UnifiedUserIdentity u } } + /// + /// Get the fully qualified resource path for a specified resource. + /// + /// The FoundationaLLM instance identifier. + /// The type of the resource. + /// The name of the resource. + /// + protected (string ResourcePath, ResourceTypeDescriptor ResourceTypeDescriptor) GetResourcePath(string instanceId, Type resourceType, string? resourceName = null) + { + if (string.IsNullOrWhiteSpace(instanceId)) + throw new ResourceProviderException( + $"The FoundationaLLM instance identifier is invalid.", + StatusCodes.Status400BadRequest); + + var resourceTypeDescriptor = + AllowedResourceTypes.Values.SingleOrDefault(art => art.ResourceType == resourceType) + ?? throw new ResourceProviderException( + $"The resource type {resourceType.Name} is not supported by the {Name} resource provider.", + StatusCodes.Status400BadRequest); + + return + ( + string.IsNullOrWhiteSpace(resourceName) + ? $"/instances/{instanceId}/providers/{this.Name}/{resourceTypeDescriptor.ResourceTypeName}" + : $"/instances/{instanceId}/providers/{this.Name}/{resourceTypeDescriptor.ResourceTypeName}/{resourceName}", + resourceTypeDescriptor + ); + } + + /// + /// Gets a object for the specified string resource path. + /// + /// The resource path. + /// Indicates whether actions are allowed in the resource path. + /// A object. + protected ResourcePath GetParsedResourcePath(string resourcePath, bool allowAction = true) => + new( + resourcePath, + _allowedResourceProviders, + _allowedResourceTypes, + allowAction: allowAction); + + private async Task UpsertResourcePostProcess( + string instanceId, + ResourceProviderUpsertResult upsertResult, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity) + { + ArgumentException.ThrowIfNullOrWhiteSpace(instanceId, nameof(instanceId)); + ArgumentNullException.ThrowIfNull(upsertResult, nameof(upsertResult)); + ArgumentNullException.ThrowIfNull(authorizationResult, nameof(authorizationResult)); + ArgumentException.ThrowIfNullOrWhiteSpace(userIdentity.UserId, nameof(userIdentity.UserId)); + ArgumentException.ThrowIfNullOrWhiteSpace(userIdentity.Name, nameof(userIdentity.Name)); + + if (!authorizationResult.Authorized) + throw new ResourceProviderException( + $"Upsert result post-processing can only be executed on authorized resources."); + + if (!authorizationResult.MustSetOwnerRoleAssignment) + return; + + if (!upsertResult.ResourceExists && Name != ResourceProviderNames.FoundationaLLM_Authorization) + { + var roleAssignmentName = Guid.NewGuid().ToString(); + var roleAssignmentDescription = $"Owner role for {userIdentity.Name}"; + var roleAssignmentResult = await _authorizationService.CreateRoleAssignment( + _instanceSettings.Id, + new RoleAssignmentRequest() + { + Name = roleAssignmentName, + Description = roleAssignmentDescription, + ObjectId = $"/instances/{instanceId}/providers/{ResourceProviderNames.FoundationaLLM_Authorization}/{AuthorizationResourceTypeNames.RoleAssignments}/{roleAssignmentName}", + PrincipalId = userIdentity.UserId, + PrincipalType = PrincipalTypes.User, + RoleDefinitionId = $"/providers/{ResourceProviderNames.FoundationaLLM_Authorization}/{AuthorizationResourceTypeNames.RoleDefinitions}/{RoleDefinitionNames.Owner}", + Scope = upsertResult.ObjectId + }, + userIdentity); + + if (!roleAssignmentResult.Success) + _logger.LogError("The [{RoleAssignment}] could not be assigned to {ObjectId}.", roleAssignmentDescription, upsertResult.ObjectId); + } + } + #endregion } } diff --git a/src/dotnet/Common/Services/Security/DependencyInjection.cs b/src/dotnet/Common/Services/Security/DependencyInjection.cs index d7a08dacdd..544283a933 100644 --- a/src/dotnet/Common/Services/Security/DependencyInjection.cs +++ b/src/dotnet/Common/Services/Security/DependencyInjection.cs @@ -19,7 +19,12 @@ public static partial class DependencyInjection public static void AddGroupMembership(this IHostApplicationBuilder builder) { // Register the Microsoft Graph API client. - builder.Services.AddSingleton(provider => new GraphServiceClient(DefaultAuthentication.AzureCredential)); + builder.Services.AddSingleton(provider => + { + var httpClient = GraphClientFactory.Create(); + httpClient.Timeout = TimeSpan.FromMinutes(15); + return new GraphServiceClient(httpClient, DefaultAuthentication.AzureCredential); + }); // Register the group membership service. builder.Services.AddSingleton(); diff --git a/src/dotnet/Common/Services/Storage/NullStorageService.cs b/src/dotnet/Common/Services/Storage/NullStorageService.cs new file mode 100644 index 0000000000..950e8daecd --- /dev/null +++ b/src/dotnet/Common/Services/Storage/NullStorageService.cs @@ -0,0 +1,37 @@ +using FoundationaLLM.Common.Interfaces; + +namespace FoundationaLLM.Common.Services.Storage +{ + /// + /// No-op implementation of the storage service. + /// + /// + /// This implementation should be used by resource providers that are using a different storage mechanism than blob storage. + /// + public class NullStorageService : IStorageService + { + /// + public string? InstanceName { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + /// + public string StorageAccountName => throw new NotImplementedException(); + + /// + public Task DeleteFileAsync(string containerName, string filePath, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + public Task FileExistsAsync(string containerName, string filePath, CancellationToken cancellationToken) => throw new NotImplementedException(); + + /// + public Task> GetFilePathsAsync(string containerName, string? directoryPath = null, bool recursive = true, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + public Task ReadFileAsync(string containerName, string filePath, CancellationToken cancellationToken) => throw new NotImplementedException(); + + /// + public Task WriteFileAsync(string containerName, string filePath, Stream fileContent, string? contentType, CancellationToken cancellationToken) => throw new NotImplementedException(); + + /// + public Task WriteFileAsync(string containerName, string filePath, string fileContent, string? contentType, CancellationToken cancellationToken) => throw new NotImplementedException(); + } +} diff --git a/src/dotnet/Common/Utils/ResourcePathUtils.cs b/src/dotnet/Common/Utils/ResourcePathUtils.cs index e41f2fec88..b3f9ea243c 100644 --- a/src/dotnet/Common/Utils/ResourcePathUtils.cs +++ b/src/dotnet/Common/Utils/ResourcePathUtils.cs @@ -78,6 +78,7 @@ private static Dictionary GetAllowedResourceType ResourceProviderNames.FoundationaLLM_Authorization => AuthorizationResourceProviderMetadata.AllowedResourceTypes, ResourceProviderNames.FoundationaLLM_AIModel => AIModelResourceProviderMetadata.AllowedResourceTypes, ResourceProviderNames.FoundationaLLM_AzureOpenAI => AzureOpenAIResourceProviderMetadata.AllowedResourceTypes, + ResourceProviderNames.FoundationaLLM_Conversation => ConversationResourceProviderMetadata.AllowedResourceTypes, _ => [] }; } diff --git a/src/dotnet/Configuration/Services/ConfigurationResourceProviderService.cs b/src/dotnet/Configuration/Services/ConfigurationResourceProviderService.cs index 61d0552239..d30721345b 100644 --- a/src/dotnet/Configuration/Services/ConfigurationResourceProviderService.cs +++ b/src/dotnet/Configuration/Services/ConfigurationResourceProviderService.cs @@ -5,6 +5,7 @@ using FoundationaLLM.Common.Exceptions; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models.Authorization; using FoundationaLLM.Common.Models.Configuration.AppConfiguration; using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.Events; @@ -58,20 +59,15 @@ public class ConfigurationResourceProviderService( logger, [ EventSetEventNamespaces.FoundationaLLM_ResourceProvider_Configuration - ]) + ], + useInternalReferencesStore: true) { /// protected override Dictionary GetResourceTypes() => ConfigurationResourceProviderMetadata.AllowedResourceTypes; - private ConcurrentDictionary _apiEndpointReferences = []; - private const string KEY_VAULT_REFERENCE_CONTENT_TYPE = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"; - private const string API_ENDPOINT_REFERENCES_FILE_NAME = "_api-endpoint-references.json"; - private const string API_ENDPOINT_REFERENCES_FILE_PATH = - $"/{ResourceProviderNames.FoundationaLLM_Configuration}/{API_ENDPOINT_REFERENCES_FILE_NAME}"; - private readonly IAzureAppConfigurationService _appConfigurationService = appConfigurationService; private readonly IAzureKeyVaultService _keyVaultService = keyVaultService; private readonly IConfigurationManager _configurationManager = configurationManager; @@ -80,93 +76,148 @@ protected override Dictionary GetResourceTypes() protected override string _name => ResourceProviderNames.FoundationaLLM_Configuration; /// - protected override async Task InitializeInternal() - { - _logger.LogInformation("Starting to initialize the {ResourceProvider} resource provider...", _name); - - if (await _storageService.FileExistsAsync(_storageContainerName, API_ENDPOINT_REFERENCES_FILE_PATH, default)) - { - var fileContent = await _storageService.ReadFileAsync( - _storageContainerName, - API_ENDPOINT_REFERENCES_FILE_PATH, - default); + protected override async Task InitializeInternal() => + await Task.CompletedTask; - var resourceReferenceStore = - JsonSerializer.Deserialize>( - Encoding.UTF8.GetString(fileContent.ToArray())); + #region Resource provider support for Management API - _apiEndpointReferences = new ConcurrentDictionary( - resourceReferenceStore!.ResourceReferences.ToDictionary(r => r.Name)); - } - else + /// + protected override async Task GetResourcesAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity, + ResourceProviderLoadOptions? options = null) => + resourcePath.MainResourceTypeName switch { - await _storageService.WriteFileAsync( - _storageContainerName, - API_ENDPOINT_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(new ResourceReferenceList + ConfigurationResourceTypeNames.AppConfigurations => await LoadAppConfigurationKeys(resourcePath.ResourceTypeInstances[0]), + ConfigurationResourceTypeNames.APIEndpointConfigurations => await LoadResources( + resourcePath.ResourceTypeInstances[0], + authorizationResult, + options ?? new ResourceProviderLoadOptions { - ResourceReferences = [] + IncludeRoles = resourcePath.IsResourceTypePath, }), - default, - default); - } - - _logger.LogInformation("The {ResourceProvider} resource provider was successfully initialized.", _name); - } - - #region Resource provider support for Management API + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest) + }; /// - protected override async Task GetResourcesAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => + resourcePath.MainResourceTypeName switch { - ConfigurationResourceTypeNames.AppConfigurations => await LoadAppConfigurationKeys(resourcePath.ResourceTypeInstances[0]), - ConfigurationResourceTypeNames.APIEndpointConfigurations => await LoadAPIEndpoints(resourcePath.ResourceTypeInstances[0]), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + ConfigurationResourceTypeNames.AppConfigurations => await UpdateAppConfigurationKey(resourcePath, serializedResource), + ConfigurationResourceTypeNames.APIEndpointConfigurations => await UpdateAPIEndpoints(resourcePath, serializedResource, userIdentity), + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }; /// - protected override async Task GetResourceInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderOptions? options = null) where T : class + protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) { - var loadResult = new List>(); + switch (resourcePath.ResourceTypeName) + { + case ConfigurationResourceTypeNames.APIEndpointConfigurations: + await DeleteResource(resourcePath); + break; + case ConfigurationResourceTypeNames.AppConfigurations: + await DeleteAppConfigurationKey(resourcePath.ResourceTypeInstances); + break; + default: + throw new ResourceProviderException( + $"The resource type {resourcePath.ResourceTypeName} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest); + }; + } + + #endregion - if (typeof(T) == typeof(APIEndpointConfiguration)) + #region Resource provider strongly typed operations + + /// + protected override async Task GetResourceAsyncInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) + { + switch (typeof(T)) { - var apiEndpoints = await LoadAPIEndpoints(resourcePath.ResourceTypeInstances[0]); - loadResult.AddRange(apiEndpoints.Select(endpoint => new ResourceProviderGetResult - { - Resource = endpoint.Resource, - Actions = endpoint.Actions, - Roles = endpoint.Roles - })); + case Type t when t == typeof(APIEndpointConfiguration): + var apiEndpoint = await LoadResource(resourcePath.ResourceId!); + return apiEndpoint + ?? throw new ResourceProviderException( + $"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} could not be loaded.", + StatusCodes.Status500InternalServerError); + case Type t when t == typeof(AppConfigurationKeyBase): + var appConfigKeys = await LoadAppConfigurationKeys(resourcePath.ResourceTypeInstances[0]); + return appConfigKeys.FirstOrDefault()?.Resource as T + ?? throw new ResourceProviderException( + $"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} could not be loaded.", + StatusCodes.Status500InternalServerError); + default: + throw new ResourceProviderException( + $"The resource type {typeof(T).Name} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest); } - else + } + + #endregion + + #region Event handling + + /// + protected override async Task HandleEvents(EventSetEventArgs e) + { + _logger.LogInformation("{EventsCount} events received in the {EventsNamespace} events namespace.", + e.Events.Count, e.Namespace); + + switch (e.Namespace) { - var appConfigKeys = await LoadAppConfigurationKeys(resourcePath.ResourceTypeInstances[0]); - loadResult.AddRange(appConfigKeys.Select(key => new ResourceProviderGetResult - { - Resource = key.Resource, - Actions = key.Actions, - Roles = key.Roles - })); + case EventSetEventNamespaces.FoundationaLLM_ResourceProvider_Configuration: + foreach (var @event in e.Events) + await HandleConfigurationResourceProviderEvent(@event); + break; + default: + // Ignore sliently any event namespace that's of no interest. + break; } - var resource = loadResult.FirstOrDefault(); - if (resource == null || resource.Resource == null) + await Task.CompletedTask; + } + + private async Task HandleConfigurationResourceProviderEvent(CloudEvent e) + { + if (string.IsNullOrWhiteSpace(e.Subject)) + return; + + try { - throw new ResourceProviderException( - $"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} was not found.", - StatusCodes.Status404NotFound); - } + var eventData = JsonSerializer.Deserialize(e.Data); + if (eventData == null) + throw new ResourceProviderException("Invalid app configuration event data."); - return resource.Resource as T - ?? throw new ResourceProviderException( - $"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} was not found."); + _logger.LogInformation("The value [{AppConfigurationKey}] managed by the [{ResourceProvider}] resource provider has changed and will be reloaded.", + eventData.Key, _name); + + var keyValue = await _appConfigurationService.GetConfigurationSettingAsync(eventData.Key); + try + { + var keyVaultSecret = JsonSerializer.Deserialize(keyValue!); + if (keyVaultSecret != null + & !string.IsNullOrWhiteSpace(keyVaultSecret!.Uri)) + keyValue = await _keyVaultService.GetSecretValueAsync( + keyVaultSecret.Uri!.Split('/').Last()); + } + catch { } + + _configurationManager[eventData.Key] = keyValue; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while handling the app configuration event."); + } } - #region Helpers for GetResourcesAsyncInternal + #endregion + + #region Resource management private async Task>> LoadAppConfigurationKeys(ResourceTypeInstance instance) { @@ -189,7 +240,7 @@ private async Task>> Loa if (string.IsNullOrEmpty(setting.Value)) { - result.Add(new ResourceProviderGetResult() { Resource = appConfig, Actions = [], Roles = [] }); + result.Add(new ResourceProviderGetResult() { Resource = appConfig, Roles = [] }); continue; } @@ -201,87 +252,12 @@ private async Task>> Loa appConfig = kvAppConfig; } - result.Add(new ResourceProviderGetResult() { Resource = appConfig, Actions = [], Roles = [] }); + result.Add(new ResourceProviderGetResult() { Resource = appConfig, Roles = [] }); } return result; } - private async Task>> LoadAPIEndpoints(ResourceTypeInstance instance) - { - if (instance.ResourceId == null) - { - var apiEndpoints = (await Task.WhenAll( - _apiEndpointReferences.Values - .Where(apie => !apie.Deleted) - .Select(apie => LoadAPIEndpoint(apie)))).ToList(); - - return apiEndpoints.Select(service => new ResourceProviderGetResult() { Resource = service, Actions = [], Roles = [] }).ToList(); - } - else - { - if (!_apiEndpointReferences.TryGetValue(instance.ResourceId, out var resourceReference) - || resourceReference.Deleted) - throw new ResourceProviderException($"Could not locate the {instance.ResourceId} api endpoint resource.", - StatusCodes.Status404NotFound); - - var apiEndpoint = await LoadAPIEndpoint(resourceReference); - - return [new ResourceProviderGetResult() { Resource = apiEndpoint, Actions = [], Roles = [] }]; - } - } - - private async Task LoadAPIEndpoint( - APIEndpointReference apiEndpointReference) - { - if (await _storageService.FileExistsAsync(_storageContainerName, apiEndpointReference.Filename, default)) - { - var fileContent = await _storageService.ReadFileAsync(_storageContainerName, apiEndpointReference.Filename, default); - - return JsonSerializer.Deserialize( - Encoding.UTF8.GetString(fileContent.ToArray()), - _serializerSettings) - ?? throw new ResourceProviderException($"Failed to load the api endpoint {apiEndpointReference.Name}.", - StatusCodes.Status400BadRequest); - } - - throw new ResourceProviderException($"Could not locate the {apiEndpointReference.Name} api endpoint resource.", - StatusCodes.Status404NotFound); - } - - #endregion - - /// - protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch - { - ConfigurationResourceTypeNames.AppConfigurations => await UpdateAppConfigurationKey(resourcePath, serializedResource), - ConfigurationResourceTypeNames.APIEndpointConfigurations => await UpdateAPIEndpoints(resourcePath, serializedResource, userIdentity), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest) - }; - - /// - protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) - { - switch (resourcePath.ResourceTypeInstances.Last().ResourceType) - { - case ConfigurationResourceTypeNames.APIEndpointConfigurations: - await DeleteAPIEndpoint(resourcePath.ResourceTypeInstances); - break; - case ConfigurationResourceTypeNames.AppConfigurations: - await DeleteAppConfigurationKey(resourcePath.ResourceTypeInstances); - break; - default: - throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances.Last().ResourceType} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest); - }; - } - - #endregion - - #region Helpers for UpsertResourceAsync - private async Task UpdateAppConfigurationKey(ResourcePath resourcePath, string serializedAppConfig) { var appConfig = JsonSerializer.Deserialize(serializedAppConfig) @@ -321,19 +297,18 @@ await _appConfigurationService.SetConfigurationSettingAsync( return new ResourceProviderUpsertResult { - ObjectId = $"/instances/{_instanceSettings.Id}/providers/{_name}/{ConfigurationResourceTypeNames.AppConfigurations}/{appConfig.Key}" + ObjectId = $"/instances/{_instanceSettings.Id}/providers/{_name}/{ConfigurationResourceTypeNames.AppConfigurations}/{appConfig.Key}", + ResourceExists = false }; } private async Task UpdateAPIEndpoints(ResourcePath resourcePath, string serializedAPIEndpoint, UnifiedUserIdentity userIdentity) { var apiEndpoint = JsonSerializer.Deserialize(serializedAPIEndpoint) - ?? throw new ResourceProviderException("The object definition is invalid."); + ?? throw new ResourceProviderException("The object definition is invalid.", + StatusCodes.Status400BadRequest); - if (_apiEndpointReferences.TryGetValue(apiEndpoint.Name!, out var existingApiEndpointReference) - && existingApiEndpointReference!.Deleted) - throw new ResourceProviderException($"The api endpoint resource {existingApiEndpointReference.Name} cannot be added or updated.", - StatusCodes.Status400BadRequest); + var existingApiEndpointReference = await _resourceReferenceStore!.GetResourceReference(apiEndpoint.Name); if (resourcePath.ResourceTypeInstances[0].ResourceId != apiEndpoint.Name) throw new ResourceProviderException("The resource path does not match the object definition (name mismatch).", @@ -349,56 +324,21 @@ private async Task UpdateAPIEndpoints(ResourcePath apiEndpoint.ObjectId = resourcePath.GetObjectId(_instanceSettings.Id, _name); + //TODO: Add validation for the API endpoint configuration + + UpdateBaseProperties(apiEndpoint, userIdentity, isNew: existingApiEndpointReference == null); if (existingApiEndpointReference == null) - apiEndpoint.CreatedBy = userIdentity.UPN; + await CreateResource(apiEndpointReference, apiEndpoint); else - apiEndpoint.UpdatedBy = userIdentity.UPN; - - await _storageService.WriteFileAsync( - _storageContainerName, - apiEndpointReference.Filename, - JsonSerializer.Serialize(apiEndpoint, _serializerSettings), - default, - default); - - _apiEndpointReferences.AddOrUpdate(apiEndpointReference.Name, apiEndpointReference, (k, v) => v); - - await _storageService.WriteFileAsync( - _storageContainerName, - API_ENDPOINT_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(new ResourceReferenceList() { ResourceReferences = _apiEndpointReferences.Values.ToList() }), - default, - default); + await SaveResource(existingApiEndpointReference, apiEndpoint); return new ResourceProviderUpsertResult { - ObjectId = (apiEndpoint as APIEndpointConfiguration)!.ObjectId + ObjectId = apiEndpoint!.ObjectId, + ResourceExists = existingApiEndpointReference != null }; } - #endregion - - #region Helpers for DeleteResourceAsync - - private async Task DeleteAPIEndpoint(List instances) - { - if (_apiEndpointReferences.TryGetValue(instances.Last().ResourceId!, out var apiEndpointReference) - || apiEndpointReference!.Deleted) - { - apiEndpointReference.Deleted = true; - - await _storageService.WriteFileAsync( - _storageContainerName, - API_ENDPOINT_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(new ResourceReferenceList() { ResourceReferences = _apiEndpointReferences.Values.ToList() }), - default, - default); - } - else - throw new ResourceProviderException($"Could not locate the {instances.Last().ResourceId} api endpoint resource.", - StatusCodes.Status404NotFound); - } - private async Task DeleteAppConfigurationKey(List instances) { string key = instances.Last().ResourceId!.Split("/").Last(); @@ -407,65 +347,6 @@ private async Task DeleteAppConfigurationKey(List instance StatusCodes.Status404NotFound); await _appConfigurationService.DeleteAppConfigurationSettingAsync(key); } - #endregion - - #region Event handling - - /// - protected override async Task HandleEvents(EventSetEventArgs e) - { - _logger.LogInformation("{EventsCount} events received in the {EventsNamespace} events namespace.", - e.Events.Count, e.Namespace); - - switch (e.Namespace) - { - case EventSetEventNamespaces.FoundationaLLM_ResourceProvider_Configuration: - foreach (var @event in e.Events) - await HandleConfigurationResourceProviderEvent(@event); - break; - default: - // Ignore sliently any event namespace that's of no interest. - break; - } - - await Task.CompletedTask; - } - - private async Task HandleConfigurationResourceProviderEvent(CloudEvent e) - { - if (string.IsNullOrWhiteSpace(e.Subject)) - return; - - try - { - var eventData = JsonSerializer.Deserialize(e.Data); - if (eventData == null) - throw new ResourceProviderException("Invalid app configuration event data."); - - _logger.LogInformation("The value [{AppConfigurationKey}] managed by the [{ResourceProvider}] resource provider has changed and will be reloaded.", - eventData.Key, _name); - - var keyValue = await _appConfigurationService.GetConfigurationSettingAsync(eventData.Key); - - try - { - var keyVaultSecret = JsonSerializer.Deserialize(keyValue!); - if (keyVaultSecret != null - & !string.IsNullOrWhiteSpace(keyVaultSecret!.Uri)) - keyValue = await _keyVaultService.GetSecretValueAsync( - keyVaultSecret.Uri!.Split('/').Last()); - } - catch { } - - _configurationManager[eventData.Key] = keyValue; - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred while handling the app configuration event."); - } - } - - #endregion private async Task TryGetAsKeyVaultReference(string keyName, string keyValue) { @@ -507,5 +388,7 @@ private async Task HandleConfigurationResourceProviderEvent(CloudEvent e) return null; } } + + #endregion } } diff --git a/src/dotnet/Conversation/Conversation.csproj b/src/dotnet/Conversation/Conversation.csproj new file mode 100644 index 0000000000..e800f294a4 --- /dev/null +++ b/src/dotnet/Conversation/Conversation.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + FoundationaLLM.Conversation + FoundationaLLM.Conversation + + + + + + + diff --git a/src/dotnet/Conversation/ResourceProviders/ConversationResourceProviderService.cs b/src/dotnet/Conversation/ResourceProviders/ConversationResourceProviderService.cs new file mode 100644 index 0000000000..0321df6eb4 --- /dev/null +++ b/src/dotnet/Conversation/ResourceProviders/ConversationResourceProviderService.cs @@ -0,0 +1,75 @@ +using FoundationaLLM.Common.Constants.ResourceProviders; +using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models.Authorization; +using FoundationaLLM.Common.Models.Configuration.Instance; +using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Services.ResourceProviders; +using FoundationaLLM.Common.Services.Storage; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace FoundationaLLM.Conversation.ResourceProviders +{ + /// + /// Implements the FoundationaLLM.Conversation resource provider. + /// + /// The options providing the with instance settings. + /// The providing authorization services. + /// The providing event services. + /// The providing the factory to create resource validators. + /// The providing Cosmos DB services. + /// The of the main dependency injection container. + /// The used for logging. + public class ConversationResourceProviderService( + IOptions instanceOptions, + IAuthorizationService authorizationService, + IEventService eventService, + IResourceValidatorFactory resourceValidatorFactory, + ICosmosDBService cosmosDBService, + IServiceProvider serviceProvider, + ILogger logger) + : ResourceProviderServiceBase( + instanceOptions.Value, + authorizationService, + new NullStorageService(), + eventService, + resourceValidatorFactory, + serviceProvider, + logger, + eventNamespacesToSubscribe: null, + useInternalReferencesStore: false) + { + private readonly ICosmosDBService _cosmosDBService = cosmosDBService; + + /// + protected override Dictionary GetResourceTypes() => + ConversationResourceProviderMetadata.AllowedResourceTypes; + + protected override string _name => ResourceProviderNames.FoundationaLLM_Conversation; + + protected override async Task InitializeInternal() => + await Task.CompletedTask; + + #region Resource provider support for Management API + + protected override async Task GetResourcesAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity, + ResourceProviderLoadOptions? options = null) => + resourcePath.MainResourceTypeName switch + { + ConversationResourceTypeNames.Conversations => await Task.FromResult(string.Empty), + _ => throw new NotImplementedException() + }; + + protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => + throw new NotImplementedException(); + + protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) => + throw new NotImplementedException(); + + #endregion + } +} diff --git a/src/dotnet/Conversation/ResourceProviders/DependencyInjection.cs b/src/dotnet/Conversation/ResourceProviders/DependencyInjection.cs new file mode 100644 index 0000000000..9b5e65ff25 --- /dev/null +++ b/src/dotnet/Conversation/ResourceProviders/DependencyInjection.cs @@ -0,0 +1,50 @@ +using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models.Configuration.Instance; +using FoundationaLLM.Conversation.ResourceProviders; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace FoundationaLLM +{ + /// + /// Provides extension methods used to configure dependency injection. + /// + public static partial class DependencyInjection + { + /// + /// Registers the FoundationaLLM.Conversation resource provider as a singleton service. + /// + /// The application builder managing the dependency injection container. + /// + /// Requires an service to be also registered with the dependency injection container. + /// + public static void AddConversationResourceProvider(this IHostApplicationBuilder builder) => + builder.Services.AddConversationResourceProvider(builder.Configuration); + + /// + /// Registers the FoundationaLLM.Conversation resource provider as a singleton service. + /// + /// The dependency injection container service collection. + /// The configuration manager. + /// + /// Requires an service to be also registered with the dependency injection container. + /// + public static void AddConversationResourceProvider(this IServiceCollection services, IConfigurationManager configuration) + { + services.AddSingleton(sp => + new ConversationResourceProviderService( + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp, + sp.GetRequiredService>())); + + services.ActivateSingleton(); + } + } +} diff --git a/src/dotnet/Core/Interfaces/ICoreService.cs b/src/dotnet/Core/Interfaces/ICoreService.cs index 2eafaa0b72..29b916c744 100644 --- a/src/dotnet/Core/Interfaces/ICoreService.cs +++ b/src/dotnet/Core/Interfaces/ICoreService.cs @@ -1,5 +1,5 @@ using FoundationaLLM.Common.Models.Authentication; -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration; using FoundationaLLM.Common.Models.Orchestration.Request; using FoundationaLLM.Common.Models.Orchestration.Response; @@ -18,7 +18,7 @@ public interface ICoreService /// Returns list of chat session ids and names. /// /// The instance id for which to retrieve chat sessions. - Task> GetAllChatSessionsAsync(string instanceId); + Task> GetAllChatSessionsAsync(string instanceId); /// /// Returns the chat messages related to an existing session. @@ -32,7 +32,7 @@ public interface ICoreService /// /// The instance Id. /// The session properties. - Task CreateNewChatSessionAsync(string instanceId, ChatSessionProperties chatSessionProperties); + Task CreateNewChatSessionAsync(string instanceId, ChatSessionProperties chatSessionProperties); /// /// Rename the chat session from its default (eg., "New Chat") to the summary provided by OpenAI. @@ -40,7 +40,7 @@ public interface ICoreService /// The instance id. /// The session id to rename. /// The session properties. - Task RenameChatSessionAsync(string instanceId, string sessionId, ChatSessionProperties chatSessionProperties); + Task RenameChatSessionAsync(string instanceId, string sessionId, ChatSessionProperties chatSessionProperties); /// /// Delete a chat session and related messages. diff --git a/src/dotnet/Core/Services/CoreService.cs b/src/dotnet/Core/Services/CoreService.cs index 4bc4fef17a..922df5585c 100644 --- a/src/dotnet/Core/Services/CoreService.cs +++ b/src/dotnet/Core/Services/CoreService.cs @@ -1,5 +1,6 @@ using FoundationaLLM.Common.Constants; using FoundationaLLM.Common.Constants.Agents; +using FoundationaLLM.Common.Constants.Chat; using FoundationaLLM.Common.Constants.Configuration; using FoundationaLLM.Common.Constants.Orchestration; using FoundationaLLM.Common.Constants.ResourceProviders; @@ -7,8 +8,8 @@ using FoundationaLLM.Common.Extensions; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Authentication; -using FoundationaLLM.Common.Models.Chat; using FoundationaLLM.Common.Models.Configuration.Branding; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration; using FoundationaLLM.Common.Models.Orchestration.Request; using FoundationaLLM.Common.Models.Orchestration.Response; @@ -35,7 +36,7 @@ namespace FoundationaLLM.Core.Services; /// /// Initializes a new instance of the class. /// -/// The Azure Cosmos DB service that contains +/// The Azure Cosmos DB service that contains /// chat sessions and messages. /// The services used to make calls to /// the downstream APIs. @@ -48,7 +49,7 @@ namespace FoundationaLLM.Core.Services; /// A dictionary of resource providers hashed by resource provider name. /// The service providing configuration settings. public partial class CoreService( - ICosmosDbService cosmosDbService, + ICosmosDBService cosmosDBService, IEnumerable downstreamAPIServices, ILogger logger, IOptions brandingSettings, @@ -58,12 +59,12 @@ public partial class CoreService( IConfiguration configuration, IHttpClientFactoryService httpClientFactory) : ICoreService { - private readonly ICosmosDbService _cosmosDbService = cosmosDbService; + private readonly ICosmosDBService _cosmosDBService = cosmosDBService; private readonly IDownstreamAPIService _gatekeeperAPIService = downstreamAPIServices.Single(das => das.APIName == HttpClientNames.GatekeeperAPI); private readonly IDownstreamAPIService _orchestrationAPIService = downstreamAPIServices.Single(das => das.APIName == HttpClientNames.OrchestrationAPI); private readonly ILogger _logger = logger; private readonly ICallContext _callContext = callContext; - private readonly string _sessionType = brandingSettings.Value.KioskMode ? SessionTypes.KioskSession : SessionTypes.Session; + private readonly string _sessionType = brandingSettings.Value.KioskMode ? ConversationTypes.KioskSession : ConversationTypes.Session; private readonly CoreServiceSettings _settings = settings.Value; private readonly IHttpClientFactoryService _httpClientFactory = httpClientFactory; private readonly string _baseUrl = GetBaseUrl(configuration, httpClientFactory, callContext).GetAwaiter().GetResult(); @@ -86,15 +87,15 @@ public partial class CoreService( .ToHashSet(); /// - public async Task> GetAllChatSessionsAsync(string instanceId) => - await _cosmosDbService.GetSessionsAsync(_sessionType, _callContext.CurrentUserIdentity?.UPN ?? + public async Task> GetAllChatSessionsAsync(string instanceId) => + await _cosmosDBService.GetSessionsAsync(_sessionType, _callContext.CurrentUserIdentity?.UPN ?? throw new InvalidOperationException("Failed to retrieve the identity of the signed in user when retrieving chat sessions.")); /// public async Task> GetChatSessionMessagesAsync(string instanceId, string sessionId) { ArgumentNullException.ThrowIfNull(sessionId); - var messages = await _cosmosDbService.GetSessionMessagesAsync(sessionId, _callContext.CurrentUserIdentity?.UPN ?? + var messages = await _cosmosDBService.GetSessionMessagesAsync(sessionId, _callContext.CurrentUserIdentity?.UPN ?? throw new InvalidOperationException("Failed to retrieve the identity of the signed in user when retrieving chat messages.")); // Get a list of all attachment IDs in the messages. @@ -110,12 +111,16 @@ public async Task> GetChatSessionMessagesAsync(string instanceId, $"/instances/{instanceId}/providers/{ResourceProviderNames.FoundationaLLM_Attachment}/{AttachmentResourceTypeNames.Attachments}/{ResourceProviderActions.Filter}", JsonSerializer.Serialize(filter), _callContext.CurrentUserIdentity!); - // Cast the result to a list of AttachmentReference objects. - var attachmentReferences = result as List ?? []; + var list = result as IEnumerator; + var attachmentReferences = new List(); + + if (list != null) + { + attachmentReferences.AddRange(from attachment in (IEnumerable) list select AttachmentDetail.FromAttachmentFile(attachment)); + } if (attachmentReferences.Count > 0) { - // Add the attachment details to the messages. foreach (var message in messages) { @@ -151,33 +156,33 @@ public async Task> GetChatSessionMessagesAsync(string instanceId, } /// - public async Task CreateNewChatSessionAsync(string instanceId, ChatSessionProperties chatSessionProperties) + public async Task CreateNewChatSessionAsync(string instanceId, ChatSessionProperties chatSessionProperties) { ArgumentException.ThrowIfNullOrEmpty(chatSessionProperties.Name); - Session session = new() + Conversation session = new() { Name = chatSessionProperties.Name, Type = _sessionType, UPN = _callContext.CurrentUserIdentity?.UPN ?? throw new InvalidOperationException("Failed to retrieve the identity of the signed in user when creating a new chat session.") }; - return await _cosmosDbService.InsertSessionAsync(session); + return await _cosmosDBService.InsertSessionAsync(session); } /// - public async Task RenameChatSessionAsync(string instanceId, string sessionId, ChatSessionProperties chatSessionProperties) + public async Task RenameChatSessionAsync(string instanceId, string sessionId, ChatSessionProperties chatSessionProperties) { ArgumentNullException.ThrowIfNull(sessionId); ArgumentException.ThrowIfNullOrEmpty(chatSessionProperties.Name); - return await _cosmosDbService.UpdateSessionNameAsync(sessionId, chatSessionProperties.Name); + return await _cosmosDBService.UpdateSessionNameAsync(sessionId, chatSessionProperties.Name); } /// public async Task DeleteChatSessionAsync(string instanceId, string sessionId) { ArgumentNullException.ThrowIfNull(sessionId); - await _cosmosDbService.DeleteSessionAndMessagesAsync(sessionId); + await _cosmosDBService.DeleteSessionAndMessagesAsync(sessionId); } /// @@ -190,7 +195,7 @@ public async Task GetChatCompletionAsync(string instanceId, Completi completionRequest = PrepareCompletionRequest(completionRequest); // Retrieve conversation, including latest prompt. - var messages = await _cosmosDbService.GetSessionMessagesAsync(completionRequest.SessionId, _callContext.CurrentUserIdentity?.UPN ?? + var messages = await _cosmosDBService.GetSessionMessagesAsync(completionRequest.SessionId, _callContext.CurrentUserIdentity?.UPN ?? throw new InvalidOperationException("Failed to retrieve the identity of the signed in user when retrieving chat completions.")); var messageHistoryList = messages .Select(message => new MessageHistoryItem(message.Sender, string.IsNullOrWhiteSpace(message.Text) ? "" : message.Text)) @@ -345,15 +350,9 @@ public async Task GetCompletionOperationResult(string instan /// public async Task UploadAttachment(string instanceId, string sessionId, AttachmentFile attachmentFile, string agentName, UnifiedUserIdentity userIdentity) { - var agentBase = await _agentResourceProvider.HandleGet( - $"/instances/{instanceId}/providers/{ResourceProviderNames.FoundationaLLM_Agent}/{AgentResourceTypeNames.Agents}/{agentName}", - userIdentity); - var aiModelBase = await _aiModelResourceProvider.HandleGet( - agentBase.AIModelObjectId!, - userIdentity); - var apiEndpointConfiguration = await _configurationResourceProvider.HandleGet( - aiModelBase.EndpointObjectId!, - userIdentity); + var agentBase = await _agentResourceProvider.GetResourceAsync(instanceId, agentName, userIdentity); + var aiModelBase = await _aiModelResourceProvider.GetResourceAsync(agentBase.AIModelObjectId!, userIdentity); + var apiEndpointConfiguration = await _configurationResourceProvider.GetResourceAsync(aiModelBase.EndpointObjectId!, userIdentity); var agentRequiresOpenAIAssistants = agentBase.HasCapability(AgentCapabilityCategoryNames.OpenAIAssistants); @@ -361,7 +360,7 @@ public async Task UploadAttachment(string instance ? ResourceProviderNames.FoundationaLLM_AzureOpenAI : null; var result = await _attachmentResourceProvider.UpsertResourceAsync( - $"/instances/{instanceId}/providers/{ResourceProviderNames.FoundationaLLM_Attachment}/attachments/{attachmentFile.Name}", + instanceId, attachmentFile, _callContext.CurrentUserIdentity!); @@ -401,7 +400,7 @@ public async Task UploadAttachment(string instance } _ = await _azureOpenAIResourceProvider.UpsertResourceAsync( - $"/instances/{instanceId}/providers/{ResourceProviderNames.FoundationaLLM_AzureOpenAI}/{AzureOpenAIResourceTypeNames.FileUserContexts}/{fileUserContextName}", + instanceId, fileUserContext, userIdentity); } @@ -419,7 +418,7 @@ public async Task UploadAttachment(string instance var userName = userIdentity.UPN?.NormalizeUserPrincipalName() ?? userIdentity.UserId; var fileUserContextName = $"{userName}-file-{instanceId.ToLower()}"; - var result = await _azureOpenAIResourceProvider.GetResource( + var result = await _azureOpenAIResourceProvider.GetResourceAsync( $"/instances/{instanceId}/providers/{ResourceProviderNames.FoundationaLLM_AzureOpenAI}/{AzureOpenAIResourceTypeNames.FileUserContexts}/{fileUserContextName}/{AzureOpenAIResourceTypeNames.FilesContent}/{fileId}", userIdentity); @@ -497,7 +496,7 @@ private IDownstreamAPIService GetDownstreamAPIService(AgentGatekeeperOverrideOpt private async Task ProcessGatekeeperOptions(CompletionRequest completionRequest) { - var agentBase = await _agentResourceProvider.HandleGet($"/{AgentResourceTypeNames.Agents}/{completionRequest.AgentName}", _callContext.CurrentUserIdentity ?? + var agentBase = await _agentResourceProvider.GetResourceAsync($"/{AgentResourceTypeNames.Agents}/{completionRequest.AgentName}", _callContext.CurrentUserIdentity ?? throw new InvalidOperationException("Failed to retrieve the identity of the signed in user when retrieving the agent settings.")); if (agentBase?.GatekeeperSettings?.UseSystemSetting == false) @@ -519,7 +518,7 @@ private async Task ProcessGatekeeperOptions(Compl /// private async Task AddSessionMessageAsync(string sessionId, Message message) { - var session = await _cosmosDbService.GetSessionAsync(sessionId); + var session = await _cosmosDBService.GetSessionAsync(sessionId); // Update session cache with tokens used. session.TokensUsed += message.Tokens; @@ -529,7 +528,7 @@ private async Task AddSessionMessageAsync(string sessionId, Message message) message.UPN = upn; // Adds the incoming message to the session and updates the session with token usage. - await _cosmosDbService.UpsertSessionBatchAsync(message, session); + await _cosmosDBService.UpsertSessionBatchAsync(message, session); } /// @@ -537,7 +536,7 @@ private async Task AddSessionMessageAsync(string sessionId, Message message) /// private async Task AddPromptCompletionMessagesAsync(string sessionId, Message promptMessage, Message completionMessage, CompletionPrompt completionPrompt) { - var session = await _cosmosDbService.GetSessionAsync(sessionId); + var session = await _cosmosDBService.GetSessionAsync(sessionId); // Update session cache with tokens used. session.TokensUsed += promptMessage.Tokens; @@ -547,7 +546,7 @@ private async Task AddPromptCompletionMessagesAsync(string sessionId, Message pr promptMessage.UPN = upn; completionMessage.UPN = upn; - await _cosmosDbService.UpsertSessionBatchAsync(promptMessage, completionMessage, completionPrompt, session); + await _cosmosDBService.UpsertSessionBatchAsync(promptMessage, completionMessage, completionPrompt, session); } /// @@ -556,7 +555,7 @@ public async Task RateMessageAsync(string instanceId, string id, string ArgumentNullException.ThrowIfNull(id); ArgumentNullException.ThrowIfNull(sessionId); - return await _cosmosDbService.UpdateMessageRatingAsync(id, sessionId, rating); + return await _cosmosDBService.UpdateMessageRatingAsync(id, sessionId, rating); } /// @@ -565,7 +564,7 @@ public async Task GetCompletionPrompt(string instanceId, strin ArgumentNullException.ThrowIfNullOrEmpty(sessionId); ArgumentNullException.ThrowIfNullOrEmpty(completionPromptId); - return await _cosmosDbService.GetCompletionPrompt(sessionId, completionPromptId); + return await _cosmosDBService.GetCompletionPrompt(sessionId, completionPromptId); } /// diff --git a/src/dotnet/Core/Services/CosmosDbChangeFeedService.cs b/src/dotnet/Core/Services/CosmosDbChangeFeedService.cs index 5387ae4c23..b62887d847 100644 --- a/src/dotnet/Core/Services/CosmosDbChangeFeedService.cs +++ b/src/dotnet/Core/Services/CosmosDbChangeFeedService.cs @@ -1,14 +1,15 @@ -using Microsoft.Azure.Cosmos; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using FoundationaLLM.Core.Interfaces; +using Azure.Identity; using FoundationaLLM.Common.Constants; -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models.Configuration.CosmosDB; +using FoundationaLLM.Common.Models.Conversation; +using FoundationaLLM.Core.Interfaces; +using Microsoft.Azure.Cosmos; using Microsoft.Azure.Cosmos.Fluent; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Polly; using Polly.Retry; -using Azure.Identity; -using FoundationaLLM.Common.Models.Configuration.CosmosDB; namespace FoundationaLLM.Core.Services { @@ -22,7 +23,7 @@ public class CosmosDbChangeFeedService : ICosmosDbChangeFeedService private ChangeFeedProcessor? _changeFeedProcessorProcessUserSessions; private readonly ILogger _logger; - private readonly ICosmosDbService _cosmosDbService; + private readonly ICosmosDBService _cosmosDBService; private readonly ResiliencePipeline _resiliencePipeline; private bool _changeFeedsInitialized = false; @@ -44,10 +45,10 @@ public class CosmosDbChangeFeedService : ICosmosDbChangeFeedService /// Thrown if any of the required settings /// are null or empty. public CosmosDbChangeFeedService(ILogger logger, - ICosmosDbService cosmosDbService, + ICosmosDBService cosmosDbService, IOptions settings) { - _cosmosDbService = cosmosDbService; + _cosmosDBService = cosmosDbService; _logger = logger; CosmosSerializationOptions options = new() @@ -91,7 +92,7 @@ public async Task StartChangeFeedProcessorsAsync() try { _changeFeedProcessorProcessUserSessions = _sessions - .GetChangeFeedProcessorBuilder("ProcessUserSessions", ProcessUserSessionsChangeFeedHandler) + .GetChangeFeedProcessorBuilder("ProcessUserSessions", ProcessUserSessionsChangeFeedHandler) .WithInstanceName($"{Guid.NewGuid()}_ProcessUserSessions") // Prefix with a unique name to allow multiple instances to run at the same time. .WithLeaseContainer(_leases) .Build(); @@ -120,12 +121,12 @@ public async Task StopChangeFeedProcessorAsync() private async Task ProcessUserSessionsChangeFeedHandler( ChangeFeedProcessorContext context, - IReadOnlyCollection input, + IReadOnlyCollection input, CancellationToken cancellationToken) { using var logScope = _logger.BeginScope("Cosmos DB Change Feed Processor: ProcessUserSessionsChangeFeedHandler"); - var sessions = input.Where(i => i.Type == nameof(Session)).ToArray(); + var sessions = input.Where(i => i.Type == nameof(Conversation)).ToArray(); _logger.LogInformation("Cosmos DB Change Feed Processor: Processing {count} changes...", sessions.Count()); @@ -136,7 +137,7 @@ await Parallel.ForEachAsync(sessions, cancellationToken, async (record, token) = await _resiliencePipeline.ExecuteAsync(async token => { - await _cosmosDbService.UpsertUserSessionAsync(record, token); + await _cosmosDBService.UpsertUserSessionAsync(record, token); }, cancellationToken); } catch (Exception ex) diff --git a/src/dotnet/Core/Services/UserProfileService.cs b/src/dotnet/Core/Services/UserProfileService.cs index 2cc770ded1..b88172270f 100644 --- a/src/dotnet/Core/Services/UserProfileService.cs +++ b/src/dotnet/Core/Services/UserProfileService.cs @@ -8,7 +8,7 @@ namespace FoundationaLLM.Core.Services /// public class UserProfileService : IUserProfileService { - private readonly ICosmosDbService _cosmosDbService; + private readonly ICosmosDBService _cosmosDbService; private readonly ILogger _logger; private readonly ICallContext _callContext; @@ -20,7 +20,7 @@ public class UserProfileService : IUserProfileService /// The logging interface used to log under the /// type name. /// Contains contextual data for the calling service. - public UserProfileService(ICosmosDbService cosmosDbService, + public UserProfileService(ICosmosDBService cosmosDbService, ILogger logger, ICallContext callContext) { diff --git a/src/dotnet/CoreAPI/Controllers/CompletionsController.cs b/src/dotnet/CoreAPI/Controllers/CompletionsController.cs index 9fcb9819da..ab820e99d6 100644 --- a/src/dotnet/CoreAPI/Controllers/CompletionsController.cs +++ b/src/dotnet/CoreAPI/Controllers/CompletionsController.cs @@ -109,6 +109,13 @@ public async Task GetCompletionOperationResult(string instan /// A list of available agents. [HttpGet("completions/agents", Name = "GetAgents")] public async Task>> GetAgents(string instanceId) => - await _agentResourceProvider.GetResourcesWithRBAC(instanceId, _callContext.CurrentUserIdentity!); + await _agentResourceProvider.GetResourcesAsync( + instanceId, + _callContext.CurrentUserIdentity!, + new ResourceProviderLoadOptions + { + IncludeRoles = true, + LoadContent = false + }); } } diff --git a/src/dotnet/CoreAPI/Controllers/SessionsController.cs b/src/dotnet/CoreAPI/Controllers/SessionsController.cs index 2bb45ecb18..5fa4a7360f 100644 --- a/src/dotnet/CoreAPI/Controllers/SessionsController.cs +++ b/src/dotnet/CoreAPI/Controllers/SessionsController.cs @@ -1,7 +1,8 @@ -using FoundationaLLM.Common.Models.Chat; +using ConversationModels = FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Core.Interfaces; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using FoundationaLLM.Common.Models.Conversation; namespace FoundationaLLM.Core.API.Controllers { @@ -29,7 +30,7 @@ public class SessionsController(ICoreService coreService, /// /// The id of the instance. [HttpGet(Name = "GetAllChatSessions")] - public async Task> GetAllChatSessions(string instanceId) => + public async Task> GetAllChatSessions(string instanceId) => await _coreService.GetAllChatSessionsAsync(instanceId); /// @@ -70,7 +71,7 @@ public async Task GetCompletionPrompt(string instanceId, strin /// The id of the instance. /// The session properties. [HttpPost(Name = "CreateNewChatSession")] - public async Task CreateNewChatSession(string instanceId, [FromBody] ChatSessionProperties chatSessionProperties) => + public async Task CreateNewChatSession(string instanceId, [FromBody] ChatSessionProperties chatSessionProperties) => await _coreService.CreateNewChatSessionAsync(instanceId, chatSessionProperties); /// @@ -80,7 +81,7 @@ public async Task CreateNewChatSession(string instanceId, [FromBody] Ch /// The id of the session to rename. /// The session properties. [HttpPost("{sessionId}/rename", Name = "RenameChatSession")] - public async Task RenameChatSession(string instanceId, string sessionId, [FromBody] ChatSessionProperties chatSessionProperties) => + public async Task RenameChatSession(string instanceId, string sessionId, [FromBody] ChatSessionProperties chatSessionProperties) => await _coreService.RenameChatSessionAsync(instanceId, sessionId, chatSessionProperties); /// diff --git a/src/dotnet/CoreAPI/CoreAPI.csproj b/src/dotnet/CoreAPI/CoreAPI.csproj index 9e37abf3b5..bc57a507ad 100644 --- a/src/dotnet/CoreAPI/CoreAPI.csproj +++ b/src/dotnet/CoreAPI/CoreAPI.csproj @@ -60,6 +60,7 @@ + diff --git a/src/dotnet/CoreAPI/Program.cs b/src/dotnet/CoreAPI/Program.cs index dc2883ac2e..3030393ea6 100644 --- a/src/dotnet/CoreAPI/Program.cs +++ b/src/dotnet/CoreAPI/Program.cs @@ -8,6 +8,7 @@ using FoundationaLLM.Common.Models.Configuration.CosmosDB; using FoundationaLLM.Common.Models.Context; using FoundationaLLM.Common.OpenAPI; +using FoundationaLLM.Common.Services; using FoundationaLLM.Common.Validation; using FoundationaLLM.Core.Interfaces; using FoundationaLLM.Core.Models.Configuration; @@ -82,8 +83,6 @@ public static void Main(string[] args) builder.Services.AddInstanceProperties(builder.Configuration); - builder.Services.AddOptions() - .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_APIEndpoints_CoreAPI_Configuration_CosmosDB)); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(AppConfigurationKeySections.FoundationaLLM_Branding)); builder.Services.AddOptions() @@ -106,25 +105,14 @@ public static void Main(string[] args) builder.AddConfigurationResourceProvider(); builder.AddAzureOpenAIResourceProvider(); builder.AddAIModelResourceProvider(); + builder.AddConversationResourceProvider(); // Register the downstream services and HTTP clients. builder.AddHttpClientFactoryService(); builder.AddDownstreamAPIService(HttpClientNames.GatekeeperAPI); builder.AddDownstreamAPIService(HttpClientNames.OrchestrationAPI); - builder.Services.AddSingleton(serviceProvider => - { - var settings = serviceProvider.GetRequiredService>().Value; - return new CosmosClientBuilder(settings.Endpoint, DefaultAuthentication.AzureCredential) - .WithSerializerOptions(new CosmosSerializationOptions - { - PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase - }) - .WithConnectionModeGateway() - .Build(); - }); - - builder.Services.AddScoped(); + builder.AddAzureCosmosDBService(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/dotnet/CoreClient/Clients/RESTClients/CompletionRestClient.cs b/src/dotnet/CoreClient/Clients/RESTClients/CompletionRestClient.cs index 238adadd08..4c4569f781 100644 --- a/src/dotnet/CoreClient/Clients/RESTClients/CompletionRestClient.cs +++ b/src/dotnet/CoreClient/Clients/RESTClients/CompletionRestClient.cs @@ -1,11 +1,11 @@ -using System.Text; -using System.Text.Json; -using Azure.Core; +using Azure.Core; using FoundationaLLM.Client.Core.Interfaces; -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration.Request; using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Agent; +using System.Text; +using System.Text.Json; namespace FoundationaLLM.Client.Core.Clients.RESTClients { diff --git a/src/dotnet/CoreClient/Clients/RESTClients/SessionRESTClient.cs b/src/dotnet/CoreClient/Clients/RESTClients/SessionRESTClient.cs index 4ea6a3841e..d47c13dec2 100644 --- a/src/dotnet/CoreClient/Clients/RESTClients/SessionRESTClient.cs +++ b/src/dotnet/CoreClient/Clients/RESTClients/SessionRESTClient.cs @@ -1,6 +1,6 @@ using Azure.Core; using FoundationaLLM.Client.Core.Interfaces; -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using System.Net.Http.Json; using System.Text.Json; @@ -27,7 +27,7 @@ public async Task CreateSessionAsync(ChatSessionProperties chatSessionPr if (responseSession.IsSuccessStatusCode) { var responseContent = await responseSession.Content.ReadAsStringAsync(); - var sessionResponse = JsonSerializer.Deserialize(responseContent, SerializerOptions); + var sessionResponse = JsonSerializer.Deserialize(responseContent, SerializerOptions); if (sessionResponse?.SessionId != null) { return sessionResponse.SessionId; @@ -87,7 +87,7 @@ public async Task> GetChatSessionMessagesAsync(string sessi } /// - public async Task> GetAllChatSessionsAsync() + public async Task> GetAllChatSessionsAsync() { var coreClient = await GetCoreClientAsync(); var responseMessage = await coreClient.GetAsync($"instances/{_instanceId}/sessions"); @@ -95,7 +95,7 @@ public async Task> GetAllChatSessionsAsync() if (responseMessage.IsSuccessStatusCode) { var responseContent = await responseMessage.Content.ReadAsStringAsync(); - var sessions = JsonSerializer.Deserialize>(responseContent, SerializerOptions); + var sessions = JsonSerializer.Deserialize>(responseContent, SerializerOptions); return sessions ?? throw new InvalidOperationException("The returned sessions are invalid."); } diff --git a/src/dotnet/CoreClient/CoreClient.cs b/src/dotnet/CoreClient/CoreClient.cs index e96118c7b4..904227c31b 100644 --- a/src/dotnet/CoreClient/CoreClient.cs +++ b/src/dotnet/CoreClient/CoreClient.cs @@ -1,7 +1,7 @@ using Azure.Core; using FoundationaLLM.Client.Core.Interfaces; -using FoundationaLLM.Common.Models.Chat; using FoundationaLLM.Common.Models.Configuration.API; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration.Request; using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Agent; diff --git a/src/dotnet/CoreClient/Interfaces/ICompletionRESTClient.cs b/src/dotnet/CoreClient/Interfaces/ICompletionRESTClient.cs index 73b341f313..f0d59166f3 100644 --- a/src/dotnet/CoreClient/Interfaces/ICompletionRESTClient.cs +++ b/src/dotnet/CoreClient/Interfaces/ICompletionRESTClient.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration.Request; using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Agent; diff --git a/src/dotnet/CoreClient/Interfaces/ICoreClient.cs b/src/dotnet/CoreClient/Interfaces/ICoreClient.cs index d9b9a1dd16..0e1bb3a5fa 100644 --- a/src/dotnet/CoreClient/Interfaces/ICoreClient.cs +++ b/src/dotnet/CoreClient/Interfaces/ICoreClient.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration.Request; using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Agent; diff --git a/src/dotnet/CoreClient/Interfaces/ISessionRESTClient.cs b/src/dotnet/CoreClient/Interfaces/ISessionRESTClient.cs index 7b20dcd538..7302c363e3 100644 --- a/src/dotnet/CoreClient/Interfaces/ISessionRESTClient.cs +++ b/src/dotnet/CoreClient/Interfaces/ISessionRESTClient.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; namespace FoundationaLLM.Client.Core.Interfaces { @@ -11,7 +11,7 @@ public interface ISessionRESTClient /// Retrieves all chat sessions. /// /// - Task> GetAllChatSessionsAsync(); + Task> GetAllChatSessionsAsync(); /// /// Sets the rating for a message. diff --git a/src/dotnet/CoreWorker/Program.cs b/src/dotnet/CoreWorker/Program.cs index 086da0f285..6c25ab38f4 100644 --- a/src/dotnet/CoreWorker/Program.cs +++ b/src/dotnet/CoreWorker/Program.cs @@ -1,7 +1,9 @@ using FoundationaLLM.Common.Authentication; using FoundationaLLM.Common.Constants; using FoundationaLLM.Common.Constants.Configuration; +using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Configuration.CosmosDB; +using FoundationaLLM.Common.Services; using FoundationaLLM.Core.Interfaces; using FoundationaLLM.Core.Services; using FoundationaLLM.Core.Worker; @@ -47,7 +49,7 @@ .Build(); }); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddApplicationInsightsTelemetryWorkerService(options => diff --git a/src/dotnet/DataSource/Models/DataSourceReference.cs b/src/dotnet/DataSource/Models/DataSourceReference.cs index 1a9405e983..f8e92d7ca5 100644 --- a/src/dotnet/DataSource/Models/DataSourceReference.cs +++ b/src/dotnet/DataSource/Models/DataSourceReference.cs @@ -15,7 +15,7 @@ public class DataSourceReference : ResourceReference /// The object type of the data source. /// [JsonIgnore] - public Type DataSourceType => + public override Type ResourceType => Type switch { DataSourceTypes.Basic => typeof(DataSourceBase), diff --git a/src/dotnet/DataSource/Models/DataSourceReferenceStore.cs b/src/dotnet/DataSource/Models/DataSourceReferenceStore.cs deleted file mode 100644 index 9c12e0fde8..0000000000 --- a/src/dotnet/DataSource/Models/DataSourceReferenceStore.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace FoundationaLLM.DataSource.Models -{ - /// - /// Models the content of the data source reference store managed by the FoundationaLLM.DataSource resource provider. - /// - public class DataSourceReferenceStore - { - /// - /// The list of all data sources registered in the system. - /// - public required List DataSourceReferences { get; set; } - /// - /// The name of the default data source. - /// - public string? DefaultDataSourceName { 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() => - DataSourceReferences.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 DataSourceReferenceStore FromDictionary(Dictionary dictionary) => - new() - { - DataSourceReferences = [.. dictionary.Values] - }; - } -} diff --git a/src/dotnet/DataSource/ResourceProviders/DataSourceResourceProviderService.cs b/src/dotnet/DataSource/ResourceProviders/DataSourceResourceProviderService.cs index 2b8942fb8c..f5c75ff08d 100644 --- a/src/dotnet/DataSource/ResourceProviders/DataSourceResourceProviderService.cs +++ b/src/dotnet/DataSource/ResourceProviders/DataSourceResourceProviderService.cs @@ -1,13 +1,12 @@ using Azure.Messaging; using FluentValidation; using FoundationaLLM.Common.Constants; -using FoundationaLLM.Common.Constants.Authorization; using FoundationaLLM.Common.Constants.Configuration; using FoundationaLLM.Common.Constants.ResourceProviders; using FoundationaLLM.Common.Exceptions; -using FoundationaLLM.Common.Extensions; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models.Authorization; using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.Events; using FoundationaLLM.Common.Models.ResourceProviders; @@ -18,8 +17,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System.Collections.Concurrent; -using System.Text; using System.Text.Json; namespace FoundationaLLM.DataSource.ResourceProviders @@ -52,410 +49,119 @@ public class DataSourceResourceProviderService( loggerFactory.CreateLogger(), [ EventSetEventNamespaces.FoundationaLLM_ResourceProvider_DataSource - ]) + ], + useInternalReferencesStore: true) { /// protected override Dictionary GetResourceTypes() => DataSourceResourceProviderMetadata.AllowedResourceTypes; - private ConcurrentDictionary _dataSourceReferences = []; - private string _defaultDataSourceName = string.Empty; - - private const string DATA_SOURCE_REFERENCES_FILE_NAME = "_data-source-references.json"; - private const string DATA_SOURCE_REFERENCES_FILE_PATH = $"/{ResourceProviderNames.FoundationaLLM_DataSource}/{DATA_SOURCE_REFERENCES_FILE_NAME}"; - /// protected override string _name => ResourceProviderNames.FoundationaLLM_DataSource; /// - protected override async Task InitializeInternal() - { - _logger.LogInformation("Starting to initialize the {ResourceProvider} resource provider...", _name); - - if (await _storageService.FileExistsAsync(_storageContainerName, DATA_SOURCE_REFERENCES_FILE_PATH, default)) - { - var fileContent = await _storageService.ReadFileAsync(_storageContainerName, DATA_SOURCE_REFERENCES_FILE_PATH, default); - var dataSourceReferenceStore = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(fileContent.ToArray())); - - _dataSourceReferences = new ConcurrentDictionary( - dataSourceReferenceStore!.ToDictionary()); - _defaultDataSourceName = dataSourceReferenceStore.DefaultDataSourceName ?? string.Empty; - } - else - { - await _storageService.WriteFileAsync( - _storageContainerName, - DATA_SOURCE_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(new DataSourceReferenceStore { DataSourceReferences = [] }), - default, - default); - } - - _logger.LogInformation("The {ResourceProvider} resource provider was successfully initialized.", _name); - } + protected override async Task InitializeInternal() => + await Task.CompletedTask; #region Resource provider support for Management API /// - protected override async Task GetResourcesAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch - { - DataSourceResourceTypeNames.DataSources => await LoadDataSources(resourcePath.ResourceTypeInstances[0], userIdentity), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest) - }; - - #region Helpers for GetResourcesAsyncInternal - - private async Task>> LoadDataSources(ResourceTypeInstance instance, UnifiedUserIdentity userIdentity) - { - var dataSources = new List(); - - if (instance.ResourceId == null) - { - dataSources = (await Task.WhenAll(_dataSourceReferences.Values - .Where(dsr => !dsr.Deleted) - .Select(dsr => LoadDataSource(dsr)))) - .Where(ds => ds != null) - .Select(ds => ds!) - .ToList(); - } - else - { - DataSourceBase? dataSource; - if (!_dataSourceReferences.TryGetValue(instance.ResourceId, out var dataSourceReference)) - { - dataSource = await LoadDataSource(null, instance.ResourceId); - if (dataSource != null) - dataSources.Add(dataSource); - } - else - { - if (dataSourceReference.Deleted) - throw new ResourceProviderException( - $"Could not locate the {instance.ResourceId} data source resource.", - StatusCodes.Status404NotFound); - - dataSource = await LoadDataSource(dataSourceReference); - if (dataSource != null) - dataSources.Add(dataSource); - } - } - - return await _authorizationService.FilterResourcesByAuthorizableAction( - _instanceSettings.Id, userIdentity, dataSources, - AuthorizableActionNames.FoundationaLLM_DataSource_DataSources_Read); - } - - private async Task LoadDataSource(DataSourceReference? dataSourceReference, string? resourceId = null) - { - if (dataSourceReference != null || !string.IsNullOrWhiteSpace(resourceId)) + protected override async Task GetResourcesAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity, + ResourceProviderLoadOptions? options = null) => + resourcePath.MainResourceTypeName switch { - dataSourceReference ??= new DataSourceReference - { - Name = resourceId!, - Type = DataSourceTypes.Basic, - Filename = $"/{_name}/{resourceId}.json", - Deleted = false - }; - if (await _storageService.FileExistsAsync(_storageContainerName, dataSourceReference.Filename, default)) - { - var fileContent = await _storageService.ReadFileAsync(_storageContainerName, dataSourceReference.Filename, default); - var dataSource = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(fileContent.ToArray()), - dataSourceReference.DataSourceType, - _serializerSettings) as DataSourceBase - ?? throw new ResourceProviderException($"Failed to load the data source {dataSourceReference.Name}.", - StatusCodes.Status400BadRequest); - - if (!string.IsNullOrWhiteSpace(resourceId)) + DataSourceResourceTypeNames.DataSources => await LoadResources( + resourcePath.ResourceTypeInstances[0], + authorizationResult, + options ?? new ResourceProviderLoadOptions { - dataSourceReference.Type = dataSource.Type!; - _dataSourceReferences.AddOrUpdate(dataSourceReference.Name, dataSourceReference, (k, v) => dataSourceReference); - } - - return dataSource; - } - - if (string.IsNullOrWhiteSpace(resourceId)) - { - // Remove the reference from the dictionary since the file does not exist. - _dataSourceReferences.TryRemove(dataSourceReference.Name, out _); - return null; - } - } - - throw new ResourceProviderException($"The {_name} resource provider could not locate a resource because of invalid resource identification parameters.", - StatusCodes.Status400BadRequest); - } - - #endregion + IncludeRoles = resourcePath.IsResourceTypePath + }), + _ => throw new ResourceProviderException( + $"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest) + }; /// protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + resourcePath.MainResourceTypeName switch { DataSourceResourceTypeNames.DataSources => await UpdateDataSource(resourcePath, serializedResource, userIdentity), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest) - }; - - #region Helpers for UpsertResourceAsync - - private async Task UpdateDataSource(ResourcePath resourcePath, string serializedDataSource, UnifiedUserIdentity userIdentity) - { - var dataSource = JsonSerializer.Deserialize(serializedDataSource) - ?? throw new ResourceProviderException("The object definition is invalid.", - StatusCodes.Status400BadRequest); - - if (_dataSourceReferences.TryGetValue(dataSource.Name!, out var existingDataSourceReference) - && existingDataSourceReference!.Deleted) - throw new ResourceProviderException($"The data source resource {existingDataSourceReference.Name} cannot be added or updated.", - StatusCodes.Status400BadRequest); - - if (resourcePath.ResourceTypeInstances[0].ResourceId != dataSource.Name) - throw new ResourceProviderException("The resource path does not match the object definition (name mismatch).", - StatusCodes.Status400BadRequest); - - var dataSourceReference = new DataSourceReference - { - Name = dataSource.Name!, - Type = dataSource.Type!, - Filename = $"/{_name}/{dataSource.Name}.json", - Deleted = false - }; - - dataSource.ObjectId = resourcePath.GetObjectId(_instanceSettings.Id, _name); - - var validator = _resourceValidatorFactory.GetValidator(dataSourceReference.DataSourceType); - if (validator is IValidator dataSourceValidator) - { - var context = new ValidationContext(dataSource); - var validationResult = await dataSourceValidator.ValidateAsync(context); - if (!validationResult.IsValid) - { - throw new ResourceProviderException($"Validation failed: {string.Join(", ", validationResult.Errors.Select(e => e.ErrorMessage))}", - StatusCodes.Status400BadRequest); - } - } - - if (existingDataSourceReference == null) - dataSource.CreatedBy = userIdentity.UPN; - else - dataSource.UpdatedBy = userIdentity.UPN; - - await _storageService.WriteFileAsync( - _storageContainerName, - dataSourceReference.Filename, - JsonSerializer.Serialize(dataSource, _serializerSettings), - default, - default); - - _dataSourceReferences.AddOrUpdate(dataSourceReference.Name, dataSourceReference, (k, v) => dataSourceReference); - - await _storageService.WriteFileAsync( - _storageContainerName, - DATA_SOURCE_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(DataSourceReferenceStore.FromDictionary(_dataSourceReferences.ToDictionary())), - default, - default); - - return new ResourceProviderUpsertResult - { - ObjectId = (dataSource as DataSourceBase)!.ObjectId + _ => throw new ResourceProviderException( + $"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest) }; - } - - #endregion /// -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected override async Task ExecuteActionAsync(ResourcePath resourcePath, string serializedAction, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances.Last().ResourceType switch + protected override async Task ExecuteActionAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + string serializedAction, + UnifiedUserIdentity userIdentity) => + resourcePath.ResourceTypeName switch { - DataSourceResourceTypeNames.DataSources => resourcePath.ResourceTypeInstances.Last().Action switch + DataSourceResourceTypeNames.DataSources => resourcePath.Action switch { - ResourceProviderActions.CheckName => CheckDataSourceName(serializedAction), - ResourceProviderActions.Filter => await Filter(serializedAction), - ResourceProviderActions.Purge => await PurgeResource(resourcePath), - _ => throw new ResourceProviderException($"The action {resourcePath.ResourceTypeInstances.Last().Action} is not supported by the {_name} resource provider.", + ResourceProviderActions.CheckName => await CheckResourceName( + JsonSerializer.Deserialize(serializedAction)!), + ResourceProviderActions.Filter => await FilterResources( + resourcePath, + JsonSerializer.Deserialize(serializedAction)!, + authorizationResult, + new ResourceProviderLoadOptions + { + LoadContent = false, + IncludeRoles = false + }), + ResourceProviderActions.Purge => await PurgeResource(resourcePath), + _ => throw new ResourceProviderException( + $"The action {resourcePath.Action} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }, _ => throw new ResourceProviderException() }; -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - - #region Helpers for ExecuteActionAsync - - private ResourceNameCheckResult CheckDataSourceName(string serializedAction) - { - var resourceName = JsonSerializer.Deserialize(serializedAction); - return _dataSourceReferences.Values.Any(ar => ar.Name.Equals(resourceName!.Name, StringComparison.OrdinalIgnoreCase)) - ? new ResourceNameCheckResult - { - Name = resourceName!.Name, - Type = resourceName.Type, - Status = NameCheckResultType.Denied, - Message = "A resource with the specified name already exists or was previously deleted and not purged." - } - : new ResourceNameCheckResult - { - Name = resourceName!.Name, - Type = resourceName.Type, - Status = NameCheckResultType.Allowed - }; - } - - private async Task> Filter(string serializedAction) - { - var resourceFilter = JsonSerializer.Deserialize(serializedAction) ?? - throw new ResourceProviderException("The object definition is invalid. Please provide a resource filter.", - StatusCodes.Status400BadRequest); - if (resourceFilter.Default.HasValue) - { - if (resourceFilter.Default.Value) - { - if (string.IsNullOrWhiteSpace(_defaultDataSourceName)) - throw new ResourceProviderException("The default data source is not set.", - StatusCodes.Status404NotFound); - - if (!_dataSourceReferences.TryGetValue(_defaultDataSourceName, out var dataSourceReference) - || dataSourceReference.Deleted) - throw new ResourceProviderException( - $"Could not locate the {_defaultDataSourceName} data source resource.", - StatusCodes.Status404NotFound); - - return [await LoadDataSource(dataSourceReference)]; - } - else - { - return - [ - .. (await Task.WhenAll( - _dataSourceReferences.Values - .Where(dsr => !dsr.Deleted && ( - string.IsNullOrWhiteSpace(_defaultDataSourceName) || - !dsr.Name.Equals(_defaultDataSourceName, StringComparison.OrdinalIgnoreCase))) - .Select(dsr => LoadDataSource(dsr)))) - ]; - } - } - else - { - // TODO: Apply other filters. - return - [ - .. (await Task.WhenAll( - _dataSourceReferences.Values - .Where(dsr => !dsr.Deleted) - .Select(dsr => LoadDataSource(dsr)))) - ]; - } - } - - private async Task PurgeResource(ResourcePath resourcePath) - { - var resourceName = resourcePath.ResourceTypeInstances.Last().ResourceId!; - if (_dataSourceReferences.TryGetValue(resourceName, out var agentReference)) - { - if (agentReference.Deleted) - { - // Delete the resource file from storage. - await _storageService.DeleteFileAsync( - _storageContainerName, - agentReference.Filename, - default); - - // Remove this resource reference from the store. - _dataSourceReferences.TryRemove(resourceName, out _); - - await _storageService.WriteFileAsync( - _storageContainerName, - DATA_SOURCE_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(DataSourceReferenceStore.FromDictionary(_dataSourceReferences.ToDictionary())), - default, - default); - - return new ResourceProviderActionResult(true); - } - else - { - throw new ResourceProviderException( - $"The {resourceName} data source resource is not soft-deleted and cannot be purged.", - StatusCodes.Status400BadRequest); - } - } - else - { - throw new ResourceProviderException($"Could not locate the {resourceName} data source resource.", - StatusCodes.Status404NotFound); - } - } - - #endregion /// protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) { - switch (resourcePath.ResourceTypeInstances.Last().ResourceType) + switch (resourcePath.ResourceTypeName) { case DataSourceResourceTypeNames.DataSources: - await DeleteDataSource(resourcePath.ResourceTypeInstances); + await DeleteResource(resourcePath); break; default: - throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances.Last().ResourceType} is not supported by the {_name} resource provider.", + throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest); }; } - #region Helpers for DeleteResourceAsync + #endregion + + #region Resource provider strongly typed operations - private async Task DeleteDataSource(List instances) + /// + protected override async Task GetResourceAsyncInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) { - if (_dataSourceReferences.TryGetValue(instances.Last().ResourceId!, out var dataSourceReference)) + switch (typeof(T)) { - if (!dataSourceReference.Deleted) - { - dataSourceReference.Deleted = true; - - await _storageService.WriteFileAsync( - _storageContainerName, - DATA_SOURCE_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(DataSourceReferenceStore.FromDictionary(_dataSourceReferences.ToDictionary())), - default, - default); - } - } - else - { - throw new ResourceProviderException($"Could not locate the {instances.Last().ResourceId} data source resource.", - StatusCodes.Status404NotFound); + case Type t when t == typeof(DataSourceBase): + var apiEndpoint = await LoadResource(resourcePath.ResourceId!); + return apiEndpoint + ?? throw new ResourceProviderException( + $"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} could not be loaded.", + StatusCodes.Status500InternalServerError); + default: + throw new ResourceProviderException( + $"The resource type {typeof(T).Name} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest); } } #endregion - #endregion - - /// - protected override async Task GetResourceInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderOptions? options = null) where T : class { - if (resourcePath.ResourceTypeInstances.Count != 1) - throw new ResourceProviderException($"Invalid resource path"); - - if (typeof(T) != typeof(DataSourceBase)) - throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({resourcePath.ResourceTypeInstances[0].ResourceType})."); - - _dataSourceReferences.TryGetValue(resourcePath.ResourceTypeInstances[0].ResourceId!, out var dataSourceReference); - if (dataSourceReference is not null && dataSourceReference.Deleted) - throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId} of type {resourcePath.ResourceTypeInstances[0].ResourceType} has been soft deleted."); - - var dataSource = await LoadDataSource(dataSourceReference, resourcePath.ResourceTypeInstances[0].ResourceId); - return dataSource as T - ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId} of type {resourcePath.ResourceTypeInstances[0].ResourceType} was not found."); - } - - #region Event handling /// @@ -480,33 +186,89 @@ protected override async Task HandleEvents(EventSetEventArgs e) private async Task HandleDataSourceResourceProviderEvent(CloudEvent e) { - if (string.IsNullOrWhiteSpace(e.Subject)) - return; + await Task.CompletedTask; + return; + + // Event handling is temporarily disabled until the updated event handling mechanism is implemented. + + //if (string.IsNullOrWhiteSpace(e.Subject)) + // return; + + //var fileName = e.Subject.Split("/").Last(); + + //_logger.LogInformation("The file [{FileName}] managed by the [{ResourceProvider}] resource provider has changed and will be reloaded.", + // fileName, _name); + + //var dataSourceReference = new DataSourceReference + //{ + // Name = Path.GetFileNameWithoutExtension(fileName), + // Filename = $"/{_name}/{fileName}", + // Type = DataSourceTypes.Basic, + // Deleted = false + //}; + + //var dataSource = await LoadDataSource(dataSourceReference); + //dataSourceReference.Name = dataSource.Name; + //dataSourceReference.Type = dataSource.Type!; - var fileName = e.Subject.Split("/").Last(); + //_dataSourceReferences.AddOrUpdate( + // dataSourceReference.Name, + // dataSourceReference, + // (k, v) => v); - _logger.LogInformation("The file [{FileName}] managed by the [{ResourceProvider}] resource provider has changed and will be reloaded.", - fileName, _name); + //_logger.LogInformation("The data source reference for the [{DataSourceName}] agent or type [{DataSourceType}] was loaded.", + // dataSourceReference.Name, dataSourceReference.Type); + } + + #endregion + + #region Resource management + + private async Task UpdateDataSource(ResourcePath resourcePath, string serializedDataSource, UnifiedUserIdentity userIdentity) + { + var dataSource = JsonSerializer.Deserialize(serializedDataSource) + ?? throw new ResourceProviderException("The object definition is invalid.", + StatusCodes.Status400BadRequest); + + var existingDataSourceReference = await _resourceReferenceStore!.GetResourceReference(dataSource.Name); + + if (resourcePath.ResourceTypeInstances[0].ResourceId != dataSource.Name) + throw new ResourceProviderException("The resource path does not match the object definition (name mismatch).", + StatusCodes.Status400BadRequest); var dataSourceReference = new DataSourceReference { - Name = Path.GetFileNameWithoutExtension(fileName), - Filename = $"/{_name}/{fileName}", - Type = DataSourceTypes.Basic, + Name = dataSource.Name!, + Type = dataSource.Type!, + Filename = $"/{_name}/{dataSource.Name}.json", Deleted = false }; - var dataSource = await LoadDataSource(dataSourceReference); - dataSourceReference.Name = dataSource.Name; - dataSourceReference.Type = dataSource.Type!; + dataSource.ObjectId = resourcePath.GetObjectId(_instanceSettings.Id, _name); - _dataSourceReferences.AddOrUpdate( - dataSourceReference.Name, - dataSourceReference, - (k, v) => v); + var validator = _resourceValidatorFactory.GetValidator(dataSourceReference.ResourceType); + if (validator is IValidator dataSourceValidator) + { + var context = new ValidationContext(dataSource); + var validationResult = await dataSourceValidator.ValidateAsync(context); + if (!validationResult.IsValid) + { + throw new ResourceProviderException($"Validation failed: {string.Join(", ", validationResult.Errors.Select(e => e.ErrorMessage))}", + StatusCodes.Status400BadRequest); + } + } - _logger.LogInformation("The data source reference for the [{DataSourceName}] agent or type [{DataSourceType}] was loaded.", - dataSourceReference.Name, dataSourceReference.Type); + UpdateBaseProperties(dataSource, userIdentity, isNew: existingDataSourceReference == null); + if (existingDataSourceReference == null) + await CreateResource(dataSourceReference, dataSource); + else + await SaveResource(existingDataSourceReference, dataSource); + + return new ResourceProviderUpsertResult + { + ObjectId = dataSource!.ObjectId, + ResourceExists = existingDataSourceReference != null + }; } #endregion diff --git a/src/dotnet/Gateway/Services/GatewayCore.cs b/src/dotnet/Gateway/Services/GatewayCore.cs index 531b45f10e..f8c4adae64 100644 --- a/src/dotnet/Gateway/Services/GatewayCore.cs +++ b/src/dotnet/Gateway/Services/GatewayCore.cs @@ -299,7 +299,7 @@ private async Task> CreateOpenAIAgentCapability(strin var fileClient = GetAzureOpenAIFileClient(azureOpenAIAccount.Endpoint); var attachmentObjectId = GetRequiredParameterValue(parameters, OpenAIAgentCapabilityParameterNames.AttachmentObjectId); - var attachmentFile = await _attachmentResourceProvider.GetResource(attachmentObjectId, userIdentity, new ResourceProviderOptions { LoadContent = true }); + var attachmentFile = await _attachmentResourceProvider.GetResourceAsync(attachmentObjectId, userIdentity, new ResourceProviderLoadOptions { LoadContent = true }); var fileResult = await fileClient.UploadFileAsync( new MemoryStream(attachmentFile.Content!), diff --git a/src/dotnet/ManagementAPI/Controllers/ResourceController.cs b/src/dotnet/ManagementAPI/Controllers/ResourceController.cs index f714757f26..c01d3ae295 100644 --- a/src/dotnet/ManagementAPI/Controllers/ResourceController.cs +++ b/src/dotnet/ManagementAPI/Controllers/ResourceController.cs @@ -41,7 +41,7 @@ await HandleRequest( resourcePath, async (resourceProviderService) => { - var result = await resourceProviderService.HandleGetAsync(resourcePath, _callContext.CurrentUserIdentity); + var result = await resourceProviderService.HandleGetAsync($"instances/{instanceId}/providers/{resourceProvider}/{resourcePath}", _callContext.CurrentUserIdentity); return new OkObjectResult(result); }); @@ -60,7 +60,10 @@ await HandleRequest( resourcePath, async (resourceProviderService) => { - var result = await resourceProviderService.HandlePostAsync(resourcePath, serializedResource.ToString()!, _callContext.CurrentUserIdentity); + var result = await resourceProviderService.HandlePostAsync( + $"instances/{instanceId}/providers/{resourceProvider}/{resourcePath}", + serializedResource.ToString()!, + _callContext.CurrentUserIdentity); return new OkObjectResult(result); }); @@ -78,7 +81,7 @@ await HandleRequest( resourcePath, async (resourceProviderService) => { - await resourceProviderService.HandleDeleteAsync(resourcePath, _callContext.CurrentUserIdentity); + await resourceProviderService.HandleDeleteAsync($"instances/{instanceId}/providers/{resourceProvider}/{resourcePath}", _callContext.CurrentUserIdentity); return new OkResult(); }); diff --git a/src/dotnet/Orchestration/Orchestration/KnowledgeManagementOrchestration.cs b/src/dotnet/Orchestration/Orchestration/KnowledgeManagementOrchestration.cs index f9fea8d27c..dd43bbaa94 100644 --- a/src/dotnet/Orchestration/Orchestration/KnowledgeManagementOrchestration.cs +++ b/src/dotnet/Orchestration/Orchestration/KnowledgeManagementOrchestration.cs @@ -1,4 +1,7 @@ -using FoundationaLLM.Common.Constants.Agents; +using FoundationaLLM.Common.Clients; +using FoundationaLLM.Common.Constants; +using FoundationaLLM.Common.Constants.Agents; +using FoundationaLLM.Common.Constants.OpenAI; using FoundationaLLM.Common.Constants.ResourceProviders; using FoundationaLLM.Common.Exceptions; using FoundationaLLM.Common.Extensions; @@ -12,10 +15,6 @@ using FoundationaLLM.Orchestration.Core.Interfaces; using Microsoft.Extensions.Logging; using System.Text.RegularExpressions; -using FoundationaLLM.Common.Constants; -using FoundationaLLM.Common.Constants.OpenAI; -using FoundationaLLM.Common.Clients; - namespace FoundationaLLM.Orchestration.Core.Orchestration { @@ -52,8 +51,6 @@ public class KnowledgeManagementOrchestration( private readonly ILogger _logger = logger; private readonly bool _dataSourceAccessDenied = dataSourceAccessDenied; private readonly string _fileUserContextName = $"{callContext.CurrentUserIdentity!.UPN?.NormalizeUserPrincipalName() ?? callContext.CurrentUserIdentity!.UserId}-file-{instanceId.ToLower()}"; - private readonly string _fileUserContextObjectId = $"/instances/{instanceId}/providers/{ResourceProviderNames.FoundationaLLM_AzureOpenAI}/{AzureOpenAIResourceTypeNames.FileUserContexts}/" - + $"{callContext.CurrentUserIdentity!.UPN?.NormalizeUserPrincipalName() ?? callContext.CurrentUserIdentity!.UserId}-file-{instanceId.ToLower()}"; private readonly IResourceProviderService _attachmentResourceProvider = resourceProviderServices[ResourceProviderNames.FoundationaLLM_Attachment]; @@ -131,10 +128,11 @@ private async Task> GetAttachmentPaths(List a var attachments = attachmentObjectIds .ToAsyncEnumerable() - .SelectAwait(async x => await _attachmentResourceProvider.GetResource(x, _callContext.CurrentUserIdentity!)); + .SelectAwait(async x => await _attachmentResourceProvider.GetResourceAsync(x, _callContext.CurrentUserIdentity!)); - var fileUserContext = await _azureOpenAIResourceProvider.GetResource( - _fileUserContextObjectId, + var fileUserContext = await _azureOpenAIResourceProvider.GetResourceAsync( + _instanceId, + _fileUserContextName, _callContext.CurrentUserIdentity!); List result = []; @@ -193,8 +191,9 @@ private async Task> TransformContentItems(List 0) { - var fileUserContext = await _azureOpenAIResourceProvider.GetResource( - _fileUserContextObjectId, + var fileUserContext = await _azureOpenAIResourceProvider.GetResourceAsync( + _instanceId, + _fileUserContextName, _callContext.CurrentUserIdentity!); foreach (var fileMapping in newFileMappings) @@ -203,7 +202,7 @@ private async Task> TransformContentItems(List( - _fileUserContextObjectId, + _instanceId, fileUserContext, _callContext.CurrentUserIdentity!); } diff --git a/src/dotnet/Orchestration/Orchestration/OrchestrationBuilder.cs b/src/dotnet/Orchestration/Orchestration/OrchestrationBuilder.cs index 75d05bd1dc..38aa17c1fc 100644 --- a/src/dotnet/Orchestration/Orchestration/OrchestrationBuilder.cs +++ b/src/dotnet/Orchestration/Orchestration/OrchestrationBuilder.cs @@ -130,20 +130,20 @@ public class OrchestrationBuilder var explodedObjects = new Dictionary(); - var agentBase = await agentResourceProvider.HandleGet( + var agentBase = await agentResourceProvider.GetResourceAsync( $"/{AgentResourceTypeNames.Agents}/{agentName}", currentUserIdentity); - var prompt = await promptResourceProvider.HandleGet( + var prompt = await promptResourceProvider.GetResourceAsync( agentBase.PromptObjectId!, currentUserIdentity); - var aiModel = await aiModelResourceProvider.HandleGet( + var aiModel = await aiModelResourceProvider.GetResourceAsync( agentBase.AIModelObjectId!, currentUserIdentity); - var apiEndpointConfiguration = await configurationResourceProvider.HandleGet( + var apiEndpointConfiguration = await configurationResourceProvider.GetResourceAsync( aiModel.EndpointObjectId!, currentUserIdentity); - var gatewayAPIEndpointConfiguration = await configurationResourceProvider.HandleGet( + var gatewayAPIEndpointConfiguration = await configurationResourceProvider.GetResourceAsync( $"/{ConfigurationResourceTypeNames.APIEndpointConfigurations}/GatewayAPI", currentUserIdentity); @@ -162,13 +162,13 @@ public class OrchestrationBuilder explodedObjects[aiModel.EndpointObjectId!] = apiEndpointConfiguration; explodedObjects[CompletionRequestObjectsKeys.GatewayAPIEndpointConfiguration] = gatewayAPIEndpointConfiguration; - var allAgents = await agentResourceProvider.GetResources(currentUserIdentity); + var allAgents = await agentResourceProvider.GetResourcesAsync(instanceId, currentUserIdentity); var allAgentsDescriptions = allAgents - .Where(a => !string.IsNullOrWhiteSpace(a.Description) && a.Name != agentBase.Name) + .Where(a => !string.IsNullOrWhiteSpace(a.Resource.Description) && a.Resource.Name != agentBase.Name) .Select(a => new { - a.Name, - a.Description + a.Resource.Name, + a.Resource.Description }) .ToDictionary(x => x.Name, x => x.Description); explodedObjects[CompletionRequestObjectsKeys.AllAgents] = allAgentsDescriptions; @@ -186,7 +186,7 @@ public class OrchestrationBuilder { try { - var dataSource = await dataSourceResourceProvider.HandleGet( + var dataSource = await dataSourceResourceProvider.GetResourceAsync( kmAgent.Vectorization.DataSourceObjectId, currentUserIdentity); @@ -207,7 +207,7 @@ public class OrchestrationBuilder continue; } - var indexingProfile = await vectorizationResourceProvider.GetResource( + var indexingProfile = await vectorizationResourceProvider.GetResourceAsync( indexingProfileName, currentUserIdentity); @@ -223,7 +223,7 @@ public class OrchestrationBuilder if(indexingProfile.Settings.TryGetValue(VectorizationSettingsNames.IndexingProfileApiEndpointConfigurationObjectId, out var apiEndpointConfigurationObjectId) == false) throw new OrchestrationException($"The API endpoint configuration object ID was not found in the settings of the indexing profile."); - var indexingProfileAPIEndpointConfiguration = await configurationResourceProvider.GetResource( + var indexingProfileAPIEndpointConfiguration = await configurationResourceProvider.GetResourceAsync( apiEndpointConfigurationObjectId, currentUserIdentity); @@ -232,7 +232,7 @@ public class OrchestrationBuilder if (!string.IsNullOrWhiteSpace(kmAgent.Vectorization.TextEmbeddingProfileObjectId)) { - var textEmbeddingProfile = await vectorizationResourceProvider.GetResource( + var textEmbeddingProfile = await vectorizationResourceProvider.GetResourceAsync( kmAgent.Vectorization.TextEmbeddingProfileObjectId, currentUserIdentity); @@ -270,13 +270,14 @@ public class OrchestrationBuilder { var assistantUserContextName = $"{currentUserIdentity.UPN?.NormalizeUserPrincipalName() ?? currentUserIdentity.UserId}-assistant-{instanceId.ToLower()}"; - if (!await azureOpenAIResourceProvider.ResourceExists( + var nameCheckResult = await azureOpenAIResourceProvider.ResourceExists( instanceId, assistantUserContextName, - AzureOpenAIResourceTypeNames.AssistantUserContexts, - currentUserIdentity)) + currentUserIdentity); + + if (!nameCheckResult.Exists) { - var result = await azureOpenAIResourceProvider.CreateOrUpdateResource( + var result = await azureOpenAIResourceProvider.UpsertResourceAsync( instanceId, new AssistantUserContext { @@ -296,7 +297,6 @@ public class OrchestrationBuilder } } }, - AzureOpenAIResourceTypeNames.AssistantUserContexts, currentUserIdentity); if (!string.IsNullOrWhiteSpace(result.NewOpenAIAssistantId)) @@ -309,10 +309,9 @@ public class OrchestrationBuilder } else { - var assistantUserContext = await azureOpenAIResourceProvider.HandleGet( + var assistantUserContext = await azureOpenAIResourceProvider.GetResourceAsync( instanceId, assistantUserContextName, - AzureOpenAIResourceTypeNames.AssistantUserContexts, currentUserIdentity); explodedObjects[CompletionRequestObjectsKeys.OpenAIAssistantId] = assistantUserContext.OpenAIAssistantId!; @@ -332,10 +331,9 @@ public class OrchestrationBuilder string.IsNullOrWhiteSpace(assistantConversation.OpenAIThreadId)) { var result = await azureOpenAIResourceProvider - .CreateOrUpdateResource( + .UpsertResourceAsync( instanceId, assistantUserContext, - AzureOpenAIResourceTypeNames.AssistantUserContexts, currentUserIdentity); if (!string.IsNullOrWhiteSpace(result.NewOpenAIAssistantThreadId)) diff --git a/src/dotnet/Orchestration/Services/LLMOrchestrationServiceManager.cs b/src/dotnet/Orchestration/Services/LLMOrchestrationServiceManager.cs index d824a61fd5..912bc075f8 100644 --- a/src/dotnet/Orchestration/Services/LLMOrchestrationServiceManager.cs +++ b/src/dotnet/Orchestration/Services/LLMOrchestrationServiceManager.cs @@ -5,12 +5,14 @@ using FoundationaLLM.Common.Exceptions; using FoundationaLLM.Common.Extensions; using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.Infrastructure; using FoundationaLLM.Common.Models.ResourceProviders.Configuration; using FoundationaLLM.Orchestration.Core.Interfaces; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System.Text.Json; namespace FoundationaLLM.Orchestration.Core.Services @@ -20,6 +22,7 @@ namespace FoundationaLLM.Orchestration.Core.Services /// public class LLMOrchestrationServiceManager : ILLMOrchestrationServiceManager { + private readonly InstanceSettings _instanceSettings; private readonly Dictionary _resourceProviderServices; private readonly IConfiguration _configuration; private readonly ILogger _logger; @@ -29,14 +32,17 @@ public class LLMOrchestrationServiceManager : ILLMOrchestrationServiceManager /// /// Creates a new instance of the LLM Orchestration Service Manager. /// + /// The options providing the with instance settings. /// A list of resource providers. /// The used to retrieve configuration values. /// The logger for the orchestration service manager. public LLMOrchestrationServiceManager( + IOptions instanceOptions, IEnumerable resourceProviderServices, IConfiguration configuration, ILogger logger) { + _instanceSettings = instanceOptions.Value; _resourceProviderServices = resourceProviderServices.ToDictionary(rps => rps.Name); _configuration = configuration; @@ -62,16 +68,17 @@ private async Task Initialize() var configurationResourceProvider = _resourceProviderServices[ResourceProviderNames.FoundationaLLM_Configuration]; await configurationResourceProvider.WaitForInitialization(); - var apiEndpointConfigurations = await configurationResourceProvider.GetResources( + var apiEndpointConfigurations = await configurationResourceProvider.GetResourcesAsync( + _instanceSettings.Id, DefaultAuthentication.ServiceIdentity!); _externalOrchestrationServiceNames = apiEndpointConfigurations - .Where(aec => aec.Category == APIEndpointCategory.ExternalOrchestration - && aec.AuthenticationParameters.TryGetValue(AuthenticationParametersKeys.APIKeyConfigurationName, out var apiKeyConfigObj) + .Where(aec => aec.Resource.Category == APIEndpointCategory.ExternalOrchestration + && aec.Resource.AuthenticationParameters.TryGetValue(AuthenticationParametersKeys.APIKeyConfigurationName, out var apiKeyConfigObj) && apiKeyConfigObj is JsonElement apiKeyConfig && !string.IsNullOrWhiteSpace(apiKeyConfig.GetString()) && apiKeyConfig.GetString()!.StartsWith(AppConfigurationKeySections.FoundationaLLM_APIEndpoints)) - .Select(aec => aec.Name) + .Select(aec => aec.Resource.Name) .ToList(); _logger.LogInformation("The LLM Orchestration Service Manager service was successfully initialized."); diff --git a/src/dotnet/Orchestration/Services/OrchestrationService.cs b/src/dotnet/Orchestration/Services/OrchestrationService.cs index b42f7ad879..9cf90e15c4 100644 --- a/src/dotnet/Orchestration/Services/OrchestrationService.cs +++ b/src/dotnet/Orchestration/Services/OrchestrationService.cs @@ -9,6 +9,7 @@ using FoundationaLLM.Common.Models.Orchestration.Request; using FoundationaLLM.Common.Models.Orchestration.Response; using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Models.ResourceProviders.Agent; using FoundationaLLM.Orchestration.Core.Interfaces; using FoundationaLLM.Orchestration.Core.Models; using FoundationaLLM.Orchestration.Core.Orchestration; @@ -220,12 +221,11 @@ private async Task ValidAgentName(string instanceId, string agentName) { var agentResourceProvider = _resourceProviderServices[ResourceProviderNames.FoundationaLLM_Agent]; - var result = await agentResourceProvider.CheckResourceName( + var nameCheckResult = await agentResourceProvider.ResourceExists( instanceId, agentName, - AgentResourceTypeNames.Agents, _callContext.CurrentUserIdentity!); - return result.Status == NameCheckResultType.Allowed; + return nameCheckResult.Exists && !nameCheckResult.Deleted; } } diff --git a/src/dotnet/Prompt/Models/Resources/PromptReference.cs b/src/dotnet/Prompt/Models/PromptReference.cs similarity index 90% rename from src/dotnet/Prompt/Models/Resources/PromptReference.cs rename to src/dotnet/Prompt/Models/PromptReference.cs index 0e9f707cb2..8d8996fe48 100644 --- a/src/dotnet/Prompt/Models/Resources/PromptReference.cs +++ b/src/dotnet/Prompt/Models/PromptReference.cs @@ -4,7 +4,7 @@ using FoundationaLLM.Common.Models.ResourceProviders.Prompt; using System.Text.Json.Serialization; -namespace FoundationaLLM.Prompt.Models.Resources +namespace FoundationaLLM.Prompt.Models { /// /// Provides details about a prompt. @@ -15,7 +15,7 @@ public class PromptReference : ResourceReference /// The object type of the agent. /// [JsonIgnore] - public Type PromptType => + public override Type ResourceType => Type switch { PromptTypes.Basic => typeof(PromptBase), diff --git a/src/dotnet/Prompt/Models/Resources/PromptReferenceStore.cs b/src/dotnet/Prompt/Models/Resources/PromptReferenceStore.cs deleted file mode 100644 index 1986ff4233..0000000000 --- a/src/dotnet/Prompt/Models/Resources/PromptReferenceStore.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace FoundationaLLM.Prompt.Models.Resources -{ - /// - /// Models the content of the prompt reference store managed by the FoundationaLLM.Prompt resource provider. - /// - public class PromptReferenceStore - { - /// - /// The list of all prompts registered in the system. - /// - public required List PromptReferences { 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() => - PromptReferences.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 PromptReferenceStore FromDictionary(Dictionary dictionary) => - new PromptReferenceStore - { - PromptReferences = dictionary.Values.ToList() - }; - } -} diff --git a/src/dotnet/Prompt/ResourceProviders/PromptResourceProviderService.cs b/src/dotnet/Prompt/ResourceProviders/PromptResourceProviderService.cs index 51d73cb726..6aeb43d085 100644 --- a/src/dotnet/Prompt/ResourceProviders/PromptResourceProviderService.cs +++ b/src/dotnet/Prompt/ResourceProviders/PromptResourceProviderService.cs @@ -3,11 +3,12 @@ using FoundationaLLM.Common.Exceptions; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models.Authorization; using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Prompt; using FoundationaLLM.Common.Services.ResourceProviders; -using FoundationaLLM.Prompt.Models.Resources; +using FoundationaLLM.Prompt.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -43,167 +44,106 @@ public class PromptResourceProviderService( eventService, resourceValidatorFactory, serviceProvider, - logger) + logger, + [], + useInternalReferencesStore: true) { /// protected override Dictionary GetResourceTypes() => PromptResourceProviderMetadata.AllowedResourceTypes; - private ConcurrentDictionary _promptReferences = []; - - private const string PROMPT_REFERENCES_FILE_NAME = "_prompt-references.json"; - private const string PROMPT_REFERENCES_FILE_PATH = $"/{ResourceProviderNames.FoundationaLLM_Prompt}/{PROMPT_REFERENCES_FILE_NAME}"; - /// protected override string _name => ResourceProviderNames.FoundationaLLM_Prompt; /// - protected override async Task InitializeInternal() - { - _logger.LogInformation("Starting to initialize the {ResourceProvider} resource provider...", _name); - - if (await _storageService.FileExistsAsync(_storageContainerName, PROMPT_REFERENCES_FILE_PATH, default)) - { - var fileContent = await _storageService.ReadFileAsync(_storageContainerName, PROMPT_REFERENCES_FILE_PATH, default); - var promptReferenceStore = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(fileContent.ToArray())); - - _promptReferences = new ConcurrentDictionary( - promptReferenceStore!.ToDictionary()); - } - else - { - await _storageService.WriteFileAsync( - _storageContainerName, - PROMPT_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(new PromptReferenceStore { PromptReferences = [] }), - default, - default); - } - - _logger.LogInformation("The {ResourceProvider} resource provider was successfully initialized.", _name); - } + protected override async Task InitializeInternal() => + await Task.CompletedTask; #region Resource provider support for Management API /// - protected override async Task GetResourcesAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + protected override async Task GetResourcesAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity, + ResourceProviderLoadOptions? options = null) => + resourcePath.MainResourceTypeName switch { - PromptResourceTypeNames.Prompts => await LoadPrompts(resourcePath.ResourceTypeInstances[0]), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + PromptResourceTypeNames.Prompts => await LoadResources( + resourcePath.ResourceTypeInstances[0], + authorizationResult, + options ?? new ResourceProviderLoadOptions + { + IncludeRoles = resourcePath.IsResourceTypePath + }), + _ => throw new ResourceProviderException( + $"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }; - #region Helpers for GetResourcesAsyncInternal - - private async Task>> LoadPrompts(ResourceTypeInstance instance) - { - if (instance.ResourceId == null) + /// + protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => + resourcePath.MainResourceTypeName switch { - var prompts = (await Task.WhenAll( - _promptReferences.Values - .Where(pr => !pr.Deleted) - .Select(pr => LoadPrompt(pr)))) - .Where(pr => pr != null) - .ToList(); + PromptResourceTypeNames.Prompts => await UpdatePrompt(resourcePath, serializedResource, userIdentity), + _ => throw new ResourceProviderException( + $"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest), + }; - return prompts.Select(prompt => new ResourceProviderGetResult() { Resource = prompt, Actions = [], Roles = [] }).ToList(); - } - else + /// + protected override async Task ExecuteActionAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + string serializedAction, + UnifiedUserIdentity userIdentity) => + resourcePath.ResourceTypeName switch { - PromptBase? prompt; - if (!_promptReferences.TryGetValue(instance.ResourceId, out var promptReference)) + PromptResourceTypeNames.Prompts => resourcePath.Action switch { - prompt = await LoadPrompt(null, instance.ResourceId); - if (prompt != null) - { - return [new ResourceProviderGetResult() { Resource = prompt, Actions = [], Roles = [] }]; - } - return []; - } - - if (promptReference.Deleted) - { - throw new ResourceProviderException( - $"Could not locate the {instance.ResourceId} prompt resource.", - StatusCodes.Status404NotFound); - } - - prompt = await LoadPrompt(promptReference); - if (prompt != null) - { - return [new ResourceProviderGetResult() { Resource = prompt, Actions = [], Roles = [] }]; - } - return []; - } - } + ResourceProviderActions.CheckName => CheckResourceName( + JsonSerializer.Deserialize(serializedAction)!), + ResourceProviderActions.Purge => await PurgeResource(resourcePath), + _ => throw new ResourceProviderException( + $"The action {resourcePath.Action} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest) + }, + _ => throw new ResourceProviderException() + }; - private async Task LoadPrompt(PromptReference? promptReference, string? resourceId = null) + /// + protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) { - if (promptReference != null || !string.IsNullOrEmpty(resourceId)) + switch (resourcePath.ResourceTypeName) { - promptReference ??= new PromptReference - { - Name = resourceId!, - Type = PromptTypes.Multipart, - Filename = $"/{_name}/{resourceId}.json", - Deleted = false - }; - if (await _storageService.FileExistsAsync(_storageContainerName, promptReference.Filename, default)) - { - var fileContent = - await _storageService.ReadFileAsync(_storageContainerName, promptReference.Filename, default); - var prompt = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(fileContent.ToArray()), - promptReference.PromptType, - _serializerSettings) as PromptBase - ?? throw new ResourceProviderException($"Failed to load the prompt {promptReference.Name}.", - StatusCodes.Status400BadRequest); - - if (!string.IsNullOrWhiteSpace(resourceId)) - { - promptReference.Type = prompt.Type!; - _promptReferences.AddOrUpdate(promptReference.Name, promptReference, (k, v) => promptReference); - } - - return prompt; - } - - if (string.IsNullOrWhiteSpace(resourceId)) - { - // Remove the reference from the dictionary since the file does not exist. - _promptReferences.TryRemove(promptReference.Name, out _); - return null; - } - } - - throw new ResourceProviderException($"The {_name} resource provider could not locate a resource because of invalid resource identification parameters.", - StatusCodes.Status400BadRequest); + case PromptResourceTypeNames.Prompts: + await DeleteResource(resourcePath); + break; + default: + throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeName} is not supported by the {_name} resource provider.", + StatusCodes.Status400BadRequest); + }; } #endregion + #region Resource provider strongly typed operations + /// - protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch - { - PromptResourceTypeNames.Prompts => await UpdatePrompt(resourcePath, serializedResource, userIdentity), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest), - }; + protected override async Task GetResourceAsyncInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) => + (await LoadResource(resourcePath.ResourceId!))!; + + #endregion - #region Helpers for UpsertResourceAsync + #region Resource management private async Task UpdatePrompt(ResourcePath resourcePath, string serializedPrompt, UnifiedUserIdentity userIdentity) { var prompt = JsonSerializer.Deserialize(serializedPrompt) - ?? throw new ResourceProviderException("The object definition is invalid."); + ?? throw new ResourceProviderException("The object definition is invalid.", + StatusCodes.Status400BadRequest); - if (_promptReferences.TryGetValue(prompt.Name!, out var existingPromptReference) - && existingPromptReference!.Deleted) - throw new ResourceProviderException($"The prompt resource {existingPromptReference.Name} cannot be added or updated.", - StatusCodes.Status400BadRequest); + var existingPromptReference = await _resourceReferenceStore!.GetResourceReference(prompt.Name); if (resourcePath.ResourceTypeInstances[0].ResourceId != prompt.Name) throw new ResourceProviderException("The resource path does not match the object definition (name mismatch).", @@ -217,152 +157,23 @@ private async Task UpdatePrompt(ResourcePath resou Deleted = false }; + // TODO: Add validation for the prompt object. + prompt.ObjectId = resourcePath.GetObjectId(_instanceSettings.Id, _name); + UpdateBaseProperties(prompt, userIdentity, isNew: existingPromptReference == null); if (existingPromptReference == null) - prompt.CreatedBy = userIdentity.UPN; + await CreateResource(promptReference, prompt); else - prompt.UpdatedBy = userIdentity.UPN; - - await _storageService.WriteFileAsync( - _storageContainerName, - promptReference.Filename, - JsonSerializer.Serialize(prompt, _serializerSettings), - default, - default); - - _promptReferences.AddOrUpdate(promptReference.Name, promptReference, (k, v) => v); - - await _storageService.WriteFileAsync( - _storageContainerName, - PROMPT_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(PromptReferenceStore.FromDictionary(_promptReferences.ToDictionary())), - default, - default); + await SaveResource(existingPromptReference, prompt); return new ResourceProviderUpsertResult { - ObjectId = (prompt as PromptBase)!.ObjectId - }; - } - - #endregion - - /// -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected override async Task ExecuteActionAsync(ResourcePath resourcePath, string serializedAction, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances.Last().ResourceType switch - { - PromptResourceTypeNames.Prompts => resourcePath.ResourceTypeInstances.Last().Action switch - { - ResourceProviderActions.CheckName => CheckPromptName(serializedAction), - ResourceProviderActions.Purge => await PurgeResource(resourcePath), - _ => throw new ResourceProviderException($"The action {resourcePath.ResourceTypeInstances.Last().Action} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest) - }, - _ => throw new ResourceProviderException() + ObjectId = prompt!.ObjectId, + ResourceExists = existingPromptReference != null }; -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - - #region Helpers for ExecuteActionAsync - - private ResourceNameCheckResult CheckPromptName(string serializedAction) - { - var resourceName = JsonSerializer.Deserialize(serializedAction); - return _promptReferences.Values.Any(ar => ar.Name == resourceName!.Name) - ? new ResourceNameCheckResult - { - Name = resourceName!.Name, - Type = resourceName.Type, - Status = NameCheckResultType.Denied, - Message = "A resource with the specified name already exists or was previously deleted and not purged." - } - : new ResourceNameCheckResult - { - Name = resourceName!.Name, - Type = resourceName.Type, - Status = NameCheckResultType.Allowed - }; } - private async Task PurgeResource(ResourcePath resourcePath) - { - var resourceName = resourcePath.ResourceTypeInstances.Last().ResourceId!; - if (_promptReferences.TryGetValue(resourceName, out var agentReference)) - { - if (agentReference.Deleted) - { - // Delete the resource file from storage. - await _storageService.DeleteFileAsync( - _storageContainerName, - agentReference.Filename, - default); - - // Remove this resource reference from the store. - _promptReferences.TryRemove(resourceName, out _); - - await _storageService.WriteFileAsync( - _storageContainerName, - PROMPT_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(PromptReferenceStore.FromDictionary(_promptReferences.ToDictionary())), - default, - default); - - return new ResourceProviderActionResult(true); - } - else - { - throw new ResourceProviderException( - $"The {resourceName} prompt resource is not soft-deleted and cannot be purged.", - StatusCodes.Status400BadRequest); - } - } - else - { - throw new ResourceProviderException($"Could not locate the {resourceName} prompt resource.", - StatusCodes.Status404NotFound); - } - } - - #endregion - - /// - protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) - { - switch (resourcePath.ResourceTypeInstances.Last().ResourceType) - { - case PromptResourceTypeNames.Prompts: - await DeletePrompt(resourcePath.ResourceTypeInstances); - break; - default: - throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances.Last().ResourceType} is not supported by the {_name} resource provider.", - StatusCodes.Status400BadRequest); - }; - } - - #region Helpers for DeleteResourceAsync - - private async Task DeletePrompt(List instances) - { - if (_promptReferences.TryGetValue(instances.Last().ResourceId!, out var promptReference) - && !promptReference.Deleted) - { - promptReference.Deleted = true; - - await _storageService.WriteFileAsync( - _storageContainerName, - PROMPT_REFERENCES_FILE_PATH, - JsonSerializer.Serialize(PromptReferenceStore.FromDictionary(_promptReferences.ToDictionary())), - default, - default); - } - else - throw new ResourceProviderException($"Could not locate the {instances.Last().ResourceId} agent resource.", - StatusCodes.Status404NotFound); - } - - #endregion - #endregion } } diff --git a/src/dotnet/SemanticKernel/Plugins/KnowledgeManagementContextPlugin.cs b/src/dotnet/SemanticKernel/Plugins/KnowledgeManagementContextPlugin.cs index 62405dfc08..5037e67551 100644 --- a/src/dotnet/SemanticKernel/Plugins/KnowledgeManagementContextPlugin.cs +++ b/src/dotnet/SemanticKernel/Plugins/KnowledgeManagementContextPlugin.cs @@ -1,6 +1,6 @@ #pragma warning disable SKEXP0001 -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Vectorization; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Memory; diff --git a/src/dotnet/Vectorization/Client/VectorizationServiceClient.cs b/src/dotnet/Vectorization/Client/VectorizationServiceClient.cs index 87185df6c0..5da6a03f04 100644 --- a/src/dotnet/Vectorization/Client/VectorizationServiceClient.cs +++ b/src/dotnet/Vectorization/Client/VectorizationServiceClient.cs @@ -31,7 +31,7 @@ public VectorizationServiceClient( } /// - public async Task ProcessRequest(VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity) + public async Task ProcessRequest(string instanceId, VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity) { var httpClient = await _httpClientFactoryService.CreateClient(HttpClientNames.VectorizationAPI, userIdentity); @@ -39,7 +39,7 @@ public async Task ProcessRequest(VectorizationRequest vecto try { - var response = await httpClient.PostAsync("vectorizationrequest", new StringContent(serializedRequest, Encoding.UTF8, "application/json")); + var response = await httpClient.PostAsync($"instances/{instanceId}/vectorization-requests", new StringContent(serializedRequest, Encoding.UTF8, "application/json")); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); diff --git a/src/dotnet/Vectorization/Extensions/VectorizationRequestExtensions.cs b/src/dotnet/Vectorization/Extensions/VectorizationRequestExtensions.cs index b8b8491297..bd4ef340a6 100644 --- a/src/dotnet/Vectorization/Extensions/VectorizationRequestExtensions.cs +++ b/src/dotnet/Vectorization/Extensions/VectorizationRequestExtensions.cs @@ -1,5 +1,4 @@ -using FoundationaLLM.Common.Constants.ResourceProviders; -using FoundationaLLM.Common.Interfaces; +using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Authentication; using FoundationaLLM.Common.Models.ResourceProviders; using FoundationaLLM.Common.Models.ResourceProviders.Vectorization; @@ -17,21 +16,21 @@ public static class VectorizationRequestExtensions /// Also updates the vectorization pipeline state if request is part of a pipeline. /// /// The vectorization request + /// The FoundationaLLM instance identifier. /// The vectorization resource provider /// The providing information about the calling user identity. public static async Task UpdateVectorizationRequestResource( this VectorizationRequest request, + string instanceId, IResourceProviderService vectorizationResourceProvider, UnifiedUserIdentity userIdentity ) { - if (request.ObjectId == null) - { - //build the minimal object id for new requests - request.ObjectId = $"/{VectorizationResourceTypeNames.VectorizationRequests}/{request.Name}"; - } // in the case of a new request, this updates the object id with the fully qualified object id, otherwise it remains the same. - var result = await vectorizationResourceProvider.UpsertResourceAsync(request.ObjectId, request, userIdentity); + var result = await vectorizationResourceProvider.UpsertResourceAsync( + instanceId, + request, + userIdentity); request.ObjectId = result.ObjectId; } diff --git a/src/dotnet/Vectorization/Extensions/VectorizationResourceProviderServiceExtensions.cs b/src/dotnet/Vectorization/Extensions/VectorizationResourceProviderServiceExtensions.cs index 635cc7177d..af15739d8a 100644 --- a/src/dotnet/Vectorization/Extensions/VectorizationResourceProviderServiceExtensions.cs +++ b/src/dotnet/Vectorization/Extensions/VectorizationResourceProviderServiceExtensions.cs @@ -36,7 +36,7 @@ public static async Task> GetActivePipelines(this Ve /// public static async Task TogglePipelineActivation(this VectorizationResourceProviderService vectorizationResourceProvider, string pipelineObjectId, bool activate, UnifiedUserIdentity userIdentity) { - var pipeline = await vectorizationResourceProvider.HandleGet(pipelineObjectId, userIdentity); + var pipeline = await vectorizationResourceProvider.GetResourceAsync(pipelineObjectId, userIdentity); if (pipeline == null || pipeline.Active == activate) // nothing to update @@ -55,7 +55,7 @@ public static async Task TogglePipelineActivation(this VectorizationResourceProv /// The providing information about the calling user identity. /// The vectorization request. public static async Task GetVectorizationRequestResource(this VectorizationResourceProviderService vectorizationResourceProvider, string requestName, UnifiedUserIdentity userIdentity) - => await vectorizationResourceProvider.HandleGet($"/{VectorizationResourceTypeNames.VectorizationRequests}/{requestName}", userIdentity); + => await vectorizationResourceProvider.GetResourceAsync($"/{VectorizationResourceTypeNames.VectorizationRequests}/{requestName}", userIdentity); } } diff --git a/src/dotnet/Vectorization/Extensions/VectorizationStateServiceExtensions.cs b/src/dotnet/Vectorization/Extensions/VectorizationStateServiceExtensions.cs index ed6b26da66..b642b9fb6e 100644 --- a/src/dotnet/Vectorization/Extensions/VectorizationStateServiceExtensions.cs +++ b/src/dotnet/Vectorization/Extensions/VectorizationStateServiceExtensions.cs @@ -48,7 +48,7 @@ public static async Task GetPipelineExecutionProce var requestProcessingStates = new List(); foreach (var vectorizationRequestObjectId in pipelineState.VectorizationRequestObjectIds) { - var vectorizationRequest = await vectorizationResourceProvider.HandleGet(vectorizationRequestObjectId, userIdentity); + var vectorizationRequest = await vectorizationResourceProvider.GetResourceAsync(vectorizationRequestObjectId, userIdentity); if (vectorizationRequest == null) { diff --git a/src/dotnet/Vectorization/Interfaces/IVectorizationRequestProcessor.cs b/src/dotnet/Vectorization/Interfaces/IVectorizationRequestProcessor.cs index e2d572daaf..c4c2a6b8b8 100644 --- a/src/dotnet/Vectorization/Interfaces/IVectorizationRequestProcessor.cs +++ b/src/dotnet/Vectorization/Interfaces/IVectorizationRequestProcessor.cs @@ -11,9 +11,10 @@ public interface IVectorizationRequestProcessor /// /// Processes an incoming vectorization request. /// + /// The FoundationaLLM instance identifier. /// The object containing the details of the vectorization request. /// The user identity. /// - Task ProcessRequest(VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity); + Task ProcessRequest(string instanceId, VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity); } } diff --git a/src/dotnet/Vectorization/Interfaces/IVectorizationService.cs b/src/dotnet/Vectorization/Interfaces/IVectorizationService.cs index 02852409bf..800fffa9b4 100644 --- a/src/dotnet/Vectorization/Interfaces/IVectorizationService.cs +++ b/src/dotnet/Vectorization/Interfaces/IVectorizationService.cs @@ -11,9 +11,10 @@ public interface IVectorizationService /// /// Processes an incoming vectorization request. /// + /// The FoundationaLLM instance identifier. /// The object containing the details of the vectorization request. /// The user identity. /// - Task ProcessRequest(VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity); + Task ProcessRequest(string instanceId, VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity); } } diff --git a/src/dotnet/Vectorization/Interfaces/IVectorizationServiceClient.cs b/src/dotnet/Vectorization/Interfaces/IVectorizationServiceClient.cs index f23aa25f77..9c34c11db2 100644 --- a/src/dotnet/Vectorization/Interfaces/IVectorizationServiceClient.cs +++ b/src/dotnet/Vectorization/Interfaces/IVectorizationServiceClient.cs @@ -11,9 +11,10 @@ public interface IVectorizationServiceClient /// /// Processes an incoming vectorization request. /// + /// The FoundationaLLM instance identifier. /// The object containing the details of the vectorization request. /// The user identity. /// The result of the request including the resource object id, success or failure plus any error messages. - Task ProcessRequest(VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity); + Task ProcessRequest(string instanceId, VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity); } } diff --git a/src/dotnet/Vectorization/Models/VectorizationRequestProcessingContext.cs b/src/dotnet/Vectorization/Models/VectorizationRequestProcessingContext.cs index 3b95c029ec..95f6e47aea 100644 --- a/src/dotnet/Vectorization/Models/VectorizationRequestProcessingContext.cs +++ b/src/dotnet/Vectorization/Models/VectorizationRequestProcessingContext.cs @@ -8,6 +8,14 @@ namespace FoundationaLLM.Vectorization.Models /// public class VectorizationRequestProcessingContext { + /// + /// Gets or sets the FoundationaLLM instance identifier. + /// + /// + /// This is the identifier of the FoundationaLLM instance that the request is associated with. + /// + public required string InstanceId { get; set; } + /// /// The message that was dequeued. /// diff --git a/src/dotnet/Vectorization/ResourceProviders/VectorizationResourceProviderService.cs b/src/dotnet/Vectorization/ResourceProviders/VectorizationResourceProviderService.cs index 36bc351dc0..135eb8e3e3 100644 --- a/src/dotnet/Vectorization/ResourceProviders/VectorizationResourceProviderService.cs +++ b/src/dotnet/Vectorization/ResourceProviders/VectorizationResourceProviderService.cs @@ -6,6 +6,7 @@ using FoundationaLLM.Common.Exceptions; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models.Authorization; using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.Events; using FoundationaLLM.Common.Models.ResourceProviders; @@ -132,8 +133,12 @@ private async Task LoadResourceStore(string resourceStoreFileP #region Resource provider support for Management API /// - protected override async Task GetResourcesAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + protected override async Task GetResourcesAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity, + ResourceProviderLoadOptions? options = null) => + resourcePath.MainResourceTypeName switch { VectorizationResourceTypeNames.TextPartitioningProfiles => await LoadResources(resourcePath.ResourceTypeInstances[0], _textPartitioningProfiles), @@ -145,7 +150,7 @@ await LoadResources(resourcePath.Reso await LoadResources(resourcePath.ResourceTypeInstances[0], _pipelines), VectorizationResourceTypeNames.VectorizationRequests => await LoadVectorizationRequestResource(resourcePath.ResourceTypeInstances[0].ResourceId!), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }; @@ -159,7 +164,7 @@ private async Task>> LoadResources !p.Deleted).ToList(); - return resources.Select(resource => new ResourceProviderGetResult() { Resource = resource, Actions = [], Roles = [] }).ToList(); + return resources.Select(resource => new ResourceProviderGetResult() { Resource = resource, Roles = [] }).ToList(); } else { @@ -169,13 +174,13 @@ private async Task>> LoadResources(instance.ResourceType switch + await LoadResourceStore(instance.ResourceTypeName switch { VectorizationResourceTypeNames.TextPartitioningProfiles => TEXT_PARTITIONING_PROFILES_FILE_PATH, VectorizationResourceTypeNames.TextEmbeddingProfiles => TEXT_EMBEDDING_PROFILES_FILE_PATH, VectorizationResourceTypeNames.IndexingProfiles => INDEXING_PROFILES_FILE_PATH, VectorizationResourceTypeNames.VectorizationPipelines => PIPELINES_FILE_PATH, - _ => throw new ResourceProviderException($"The resource type {instance.ResourceType} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The resource type {instance.ResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }, resourceStore); resourceStore.TryGetValue(instance.ResourceId, out resource); @@ -185,7 +190,7 @@ private async Task>> LoadResources() { Resource = resource, Actions = [], Roles = [] }]; + return [new ResourceProviderGetResult() { Resource = resource, Roles = [] }]; } } private async Task>> LoadVectorizationRequestResource(string resourceId) @@ -203,7 +208,6 @@ private async Task>> LoadVe ResourceProviderGetResult result = new ResourceProviderGetResult { Resource = resource, - Actions = [], Roles = [] }; return [result]; @@ -216,7 +220,7 @@ private async Task>> LoadVe /// protected override async Task UpsertResourceAsync(ResourcePath resourcePath, string serializedResource, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances[0].ResourceType switch + resourcePath.MainResourceTypeName switch { VectorizationResourceTypeNames.TextPartitioningProfiles => await UpdateResource(resourcePath, serializedResource, userIdentity, _textPartitioningProfiles, TEXT_PARTITIONING_PROFILES_FILE_PATH), @@ -228,7 +232,7 @@ await UpdateResource(resourcePath, se await UpdateResource(resourcePath, serializedResource, userIdentity, _pipelines, PIPELINES_FILE_PATH), VectorizationResourceTypeNames.VectorizationRequests => await UpdateVectorizationRequestResource(resourcePath, serializedResource, userIdentity), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }; @@ -280,7 +284,8 @@ await _storageService.WriteFileAsync( return new ResourceProviderUpsertResult { - ObjectId = resource.ObjectId + ObjectId = resource.ObjectId, + ResourceExists = existingResource != null }; } @@ -307,7 +312,8 @@ private async Task UpdateVectorizationRequestResou await UpdateVectorizationRequest(resourcePath, resource, userIdentity); return new ResourceProviderUpsertResult { - ObjectId = resource.ObjectId + ObjectId = resource.ObjectId, + ResourceExists = false }; } @@ -315,44 +321,47 @@ private async Task UpdateVectorizationRequestResou /// #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected override async Task ExecuteActionAsync(ResourcePath resourcePath, string serializedAction, UnifiedUserIdentity userIdentity) => - resourcePath.ResourceTypeInstances.Last().ResourceType switch + protected override async Task ExecuteActionAsync( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + string serializedAction, UnifiedUserIdentity userIdentity) => + resourcePath.ResourceTypeName switch { - VectorizationResourceTypeNames.IndexingProfiles => resourcePath.ResourceTypeInstances.Last().Action switch + VectorizationResourceTypeNames.IndexingProfiles => resourcePath.Action switch { ResourceProviderActions.CheckName => CheckProfileName(serializedAction, _indexingProfiles), ResourceProviderActions.Filter => Filter(serializedAction, _indexingProfiles, _defaultIndexingProfileName), ResourceProviderActions.Purge => await PurgeResource(resourcePath, _indexingProfiles, INDEXING_PROFILES_FILE_PATH), - _ => throw new ResourceProviderException($"The action {resourcePath.ResourceTypeInstances.Last().Action} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The action {resourcePath.Action} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }, - VectorizationResourceTypeNames.VectorizationPipelines => resourcePath.ResourceTypeInstances.Last().Action switch + VectorizationResourceTypeNames.VectorizationPipelines => resourcePath.Action switch { - VectorizationResourceProviderActions.Activate => await SetPipelineActivation(resourcePath.ResourceTypeInstances.Last().ResourceId!, true), - VectorizationResourceProviderActions.Deactivate => await SetPipelineActivation(resourcePath.ResourceTypeInstances.Last().ResourceId!, false), + VectorizationResourceProviderActions.Activate => await SetPipelineActivation(resourcePath.ResourceId!, true), + VectorizationResourceProviderActions.Deactivate => await SetPipelineActivation(resourcePath.ResourceId!, false), ResourceProviderActions.Purge => await PurgeResource(resourcePath, _pipelines, PIPELINES_FILE_PATH), - _ => throw new ResourceProviderException($"The action {resourcePath.ResourceTypeInstances.Last().Action} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The action {resourcePath.Action} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }, - VectorizationResourceTypeNames.TextPartitioningProfiles => resourcePath.ResourceTypeInstances.Last().Action switch + VectorizationResourceTypeNames.TextPartitioningProfiles => resourcePath.Action switch { ResourceProviderActions.Purge => await PurgeResource(resourcePath, _textPartitioningProfiles, TEXT_PARTITIONING_PROFILES_FILE_PATH), - _ => throw new ResourceProviderException($"The action {resourcePath.ResourceTypeInstances.Last().Action} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The action {resourcePath.Action} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }, - VectorizationResourceTypeNames.TextEmbeddingProfiles => resourcePath.ResourceTypeInstances.Last().Action switch + VectorizationResourceTypeNames.TextEmbeddingProfiles => resourcePath.Action switch { ResourceProviderActions.Purge => await PurgeResource(resourcePath, _textEmbeddingProfiles, TEXT_EMBEDDING_PROFILES_FILE_PATH), - _ => throw new ResourceProviderException($"The action {resourcePath.ResourceTypeInstances.Last().Action} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The action {resourcePath.Action} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }, - VectorizationResourceTypeNames.VectorizationRequests => resourcePath.ResourceTypeInstances.Last().Action switch + VectorizationResourceTypeNames.VectorizationRequests => resourcePath.Action switch { - VectorizationResourceProviderActions.Process => await ProcessVectorizationRequest(resourcePath, userIdentity), - _ => throw new ResourceProviderException($"The action {resourcePath.ResourceTypeInstances.Last().Action} is not supported by the {_name} resource provider.", + VectorizationResourceProviderActions.Process => await ProcessVectorizationRequest(resourcePath, authorizationResult, userIdentity), + _ => throw new ResourceProviderException($"The action {resourcePath.Action} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }, - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} does not support actions in the {_name} resource provider.", + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} does not support actions in the {_name} resource provider.", StatusCodes.Status400BadRequest) }; #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously @@ -391,7 +400,15 @@ await _storageService.WriteFileAsync( /// The JSON string payload to pass in as an action parameter. /// public async Task ExecuteActionAsync(string resourcePath, string? serializedAction = null) => - await ExecuteActionAsync(GetResourcePath(resourcePath), serializedAction??string.Empty, GetUnifiedUserIdentity()); + await ExecuteActionAsync( + GetParsedResourcePath(resourcePath), + new ResourcePathAuthorizationResult + { + ResourcePath = resourcePath, + Authorized = true + }, + serializedAction ?? string.Empty, + GetUnifiedUserIdentity()); /// @@ -400,7 +417,7 @@ public async Task ExecuteActionAsync(string resourcePath, string? serial /// The resource path from which to retrieve resources. /// List of vectorization resources. public async Task GetResourcesAsync(string resourcePath) => - await GetResourcesAsync(GetResourcePath(resourcePath), GetUnifiedUserIdentity()); + await HandleGetAsync(resourcePath, GetUnifiedUserIdentity()); /// @@ -410,17 +427,21 @@ public async Task GetResourcesAsync(string resourcePath) => /// The user identity. /// Vectorization result /// - private async Task ProcessVectorizationRequest(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) + private async Task ProcessVectorizationRequest( + ResourcePath resourcePath, + ResourcePathAuthorizationResult authorizationResult, + UnifiedUserIdentity userIdentity) { var vectorizationRequestId = resourcePath.ResourceTypeInstances[0].ResourceId!; - var result = (List>)(await GetResourcesAsync(resourcePath, GetUnifiedUserIdentity())); //should only return one or none + var result = (List>)(await GetResourcesAsync(resourcePath, authorizationResult, GetUnifiedUserIdentity())); //should only return one or none + if (result.Count == 0) throw new ResourceProviderException($"The resource {vectorizationRequestId} was not found.", StatusCodes.Status404NotFound); var request = result.First().Resource; var requestProcessor = serviceProvider.GetService(); - var response = await requestProcessor!.ProcessRequest(request, userIdentity); + var response = await requestProcessor!.ProcessRequest(resourcePath.InstanceId!, request, userIdentity); return response; } @@ -428,19 +449,24 @@ private ResourceNameCheckResult CheckProfileName(string serializedAction, Con where T : VectorizationProfileBase { var resourceName = JsonSerializer.Deserialize(serializedAction); - return profileStore.Values.Any(p => p.Name == resourceName!.Name) + var vectorizationProfile = profileStore.Values.SingleOrDefault(p => p.Name == resourceName!.Name); + return vectorizationProfile != null ? new ResourceNameCheckResult { Name = resourceName!.Name, Type = resourceName.Type, Status = NameCheckResultType.Denied, + Exists = true, + Deleted = vectorizationProfile.Deleted, Message = "A resource with the specified name already exists or was previously deleted and not purged." } : new ResourceNameCheckResult { Name = resourceName!.Name, Type = resourceName.Type, - Status = NameCheckResultType.Allowed + Status = NameCheckResultType.Allowed, + Exists = false, + Deleted = false }; } @@ -450,9 +476,9 @@ private List Filter(string serializedAction, Concur var resourceFilter = JsonSerializer.Deserialize(serializedAction) ?? throw new ResourceProviderException("The object definition is invalid. Please provide a resource filter.", StatusCodes.Status400BadRequest); - if (resourceFilter.Default.HasValue) + if (resourceFilter.DefaultResource.HasValue) { - if (resourceFilter.Default.Value) + if (resourceFilter.DefaultResource.Value) { if (string.IsNullOrWhiteSpace(defaultProfileName)) throw new ResourceProviderException("The default profile name is not set.", @@ -495,7 +521,7 @@ private async Task PurgeResource( where T : TBase where TBase : ResourceBase { - var resourceName = resourcePath.ResourceTypeInstances.Last().ResourceId!; + var resourceName = resourcePath.ResourceId!; if (resourceStore.TryGetValue(resourceName, out var agentReference)) { if (agentReference.Deleted) @@ -531,7 +557,7 @@ await _storageService.WriteFileAsync( /// protected override async Task DeleteResourceAsync(ResourcePath resourcePath, UnifiedUserIdentity userIdentity) { - switch (resourcePath.ResourceTypeInstances.Last().ResourceType) + switch (resourcePath.ResourceTypeName) { case VectorizationResourceTypeNames.TextPartitioningProfiles: await DeleteResource(resourcePath, _textPartitioningProfiles, TEXT_PARTITIONING_PROFILES_FILE_PATH); @@ -546,7 +572,7 @@ protected override async Task DeleteResourceAsync(ResourcePath resourcePath, Uni await DeleteResource(resourcePath, _pipelines, PIPELINES_FILE_PATH); break; default: - throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest); }; } @@ -579,8 +605,8 @@ await _storageService.WriteFileAsync( #endregion /// - protected override async Task GetResourceInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderOptions? options = null) where T : class => - resourcePath.ResourceTypeInstances[0].ResourceType switch + protected override async Task GetResourceAsyncInternal(ResourcePath resourcePath, UnifiedUserIdentity userIdentity, ResourceProviderLoadOptions? options = null) where T : class => + resourcePath.MainResourceTypeName switch { VectorizationResourceTypeNames.TextPartitioningProfiles => await GetTextPartitioningProfile(resourcePath), VectorizationResourceTypeNames.TextEmbeddingProfiles => await GetTextEmbeddingProfile(resourcePath), @@ -588,7 +614,7 @@ protected override async Task GetResourceInternal(ResourcePath resourcePat VectorizationResourceTypeNames.VectorizationPipelines => await GetVectorizationProfile(resourcePath), VectorizationResourceTypeNames.VectorizationRequests => await GetVectorizationRequest(resourcePath), - _ => throw new ResourceProviderException($"The resource type {resourcePath.ResourceTypeInstances[0].ResourceType} is not supported by the {_name} resource provider.", + _ => throw new ResourceProviderException($"The resource type {resourcePath.MainResourceTypeName} is not supported by the {_name} resource provider.", StatusCodes.Status400BadRequest) }; @@ -601,12 +627,12 @@ private async Task GetTextPartitioningProfile(ResourcePath resourcePath) w throw new ResourceProviderException($"Invalid resource path"); 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 ({resourcePath.ResourceTypeInstances[0].ResourceType})."); + throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({resourcePath.MainResourceTypeName})."); var textPartitioningProfileGetResult = await LoadResources(resourcePath.ResourceTypeInstances[0], _textPartitioningProfiles); var textPartitioningProfile = textPartitioningProfileGetResult.FirstOrDefault()?.Resource; return textPartitioningProfile as T - ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} was not found."); + ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.MainResourceTypeName} was not found."); } private async Task GetTextEmbeddingProfile(ResourcePath resourcePath) where T : class @@ -615,12 +641,12 @@ private async Task GetTextEmbeddingProfile(ResourcePath resourcePath) wher throw new ResourceProviderException($"Invalid resource path"); 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 ({resourcePath.ResourceTypeInstances[0].ResourceType})."); + throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({resourcePath.MainResourceTypeName})."); var textEmbeddingProfileGetResult = await LoadResources(resourcePath.ResourceTypeInstances[0], _textEmbeddingProfiles); var textEmbeddingProfile = textEmbeddingProfileGetResult.FirstOrDefault()?.Resource; return textEmbeddingProfile as T - ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} was not found."); + ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.MainResourceTypeName} was not found."); } private async Task GetIndexingProfile(ResourcePath resourcePath) where T : class @@ -629,13 +655,13 @@ private async Task GetIndexingProfile(ResourcePath resourcePath) where T : throw new ResourceProviderException($"Invalid resource path"); 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 ({resourcePath.ResourceTypeInstances[0].ResourceType})."); + throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({resourcePath.MainResourceTypeName})."); var indexingProfileGetResult = await LoadResources(resourcePath.ResourceTypeInstances[0], _indexingProfiles); var indexingProfile = indexingProfileGetResult.FirstOrDefault()?.Resource; return indexingProfile as T - ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} was not found."); + ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.MainResourceTypeName} was not found."); } private async Task GetVectorizationProfile(ResourcePath resourcePath) where T : class @@ -644,13 +670,13 @@ private async Task GetVectorizationProfile(ResourcePath resourcePath) wher throw new ResourceProviderException($"Invalid resource path"); if (typeof(T) != typeof(VectorizationPipeline)) - throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({resourcePath.ResourceTypeInstances[0].ResourceType})."); + throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({resourcePath.MainResourceTypeName})."); var pipelineGetResult = await LoadResources(resourcePath.ResourceTypeInstances[0], _pipelines); var pipeline = pipelineGetResult.FirstOrDefault()?.Resource; return pipeline as T - ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} was not found."); + ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.MainResourceTypeName} was not found."); } private async Task GetVectorizationRequest(ResourcePath resourcePath) where T : class @@ -659,7 +685,7 @@ private async Task GetVectorizationRequest(ResourcePath resourcePath) wher throw new ResourceProviderException($"Invalid resource path"); if (typeof(T) != typeof(VectorizationRequest)) - throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({resourcePath.ResourceTypeInstances[0].ResourceType})."); + throw new ResourceProviderException($"The type of requested resource ({typeof(T)}) does not match the resource type specified in the path ({resourcePath.MainResourceTypeName})."); var vectorizationRequestList = await LoadVectorizationRequestResource(resourcePath.ResourceTypeInstances[0].ResourceId!); if (vectorizationRequestList != null && vectorizationRequestList.Count == 1) @@ -668,7 +694,7 @@ private async Task GetVectorizationRequest(ResourcePath resourcePath) wher ?? throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} is invalid."); } - throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.ResourceTypeInstances[0].ResourceType} was not found."); + throw new ResourceProviderException($"The resource {resourcePath.ResourceTypeInstances[0].ResourceId!} of type {resourcePath.MainResourceTypeName} was not found."); } #endregion @@ -718,7 +744,8 @@ await _storageService.WriteFileAsync( return new ResourceProviderUpsertResult { - ObjectId = request.ObjectId + ObjectId = request.ObjectId, + ResourceExists = false }; } @@ -738,7 +765,7 @@ private async Task ValidateContentIdentifierWithDataSource(VectorizationRe throw new ResourceProviderException($"The {ResourceProviderNames.FoundationaLLM_DataSource} resource provider was not loaded.", StatusCodes.Status400BadRequest); - var dataSource = await dataSourceResourceProviderService.GetResource(request.ContentIdentifier.DataSourceObjectId, userIdentity) + var dataSource = await dataSourceResourceProviderService.GetResourceAsync(request.ContentIdentifier.DataSourceObjectId, userIdentity) ?? throw new ResourceProviderException($"The data source {request.ContentIdentifier.DataSourceObjectId} was not found.", StatusCodes.Status400BadRequest); diff --git a/src/dotnet/Vectorization/Services/ContentSources/ContentSourceServiceFactory.cs b/src/dotnet/Vectorization/Services/ContentSources/ContentSourceServiceFactory.cs index 9a1465a8bd..61d8dc46a4 100644 --- a/src/dotnet/Vectorization/Services/ContentSources/ContentSourceServiceFactory.cs +++ b/src/dotnet/Vectorization/Services/ContentSources/ContentSourceServiceFactory.cs @@ -47,7 +47,7 @@ public async Task GetService(string serviceName, UnifiedU if (dataSourceResourceProviderService == null) throw new VectorizationException($"The resource provider {ResourceProviderNames.FoundationaLLM_DataSource} was not loaded."); - var dataSource = await dataSourceResourceProviderService.GetResource(serviceName, userIdentity); + var dataSource = await dataSourceResourceProviderService.GetResourceAsync(serviceName, userIdentity); return dataSource == null ? throw new VectorizationException($"The data source {serviceName} was not found.") : dataSource.Type switch diff --git a/src/dotnet/Vectorization/Services/Pipelines/PipelineExecutionService.cs b/src/dotnet/Vectorization/Services/Pipelines/PipelineExecutionService.cs index 39ec64252a..6c8dec10ac 100644 --- a/src/dotnet/Vectorization/Services/Pipelines/PipelineExecutionService.cs +++ b/src/dotnet/Vectorization/Services/Pipelines/PipelineExecutionService.cs @@ -20,24 +20,29 @@ using FoundationaLLM.Common.Constants.Authentication; using FoundationaLLM.Common.Authentication; using FoundationaLLM.Common.Constants.Configuration; +using Microsoft.Extensions.Options; +using FoundationaLLM.Common.Models.Configuration.Instance; namespace FoundationaLLM.Vectorization.Services.Pipelines { /// /// Executes active vectorization data pipelines. /// + /// The value providing settings. /// The global configuration provider. /// The providing dependency injection services.. /// The list of resurce providers registered with the main dependency injection container. /// Factory responsible for creating loggers. /// The used for logging. public class PipelineExecutionService( + IOptions instanceOptions, IConfiguration configuration, IServiceProvider serviceProvider, IEnumerable resourceProviderServices, ILoggerFactory loggerFactory, ILogger logger) : IPipelineExecutionService { + private readonly InstanceSettings _instanceSettings = instanceOptions.Value; private readonly IConfiguration _configuration = configuration; private readonly IServiceProvider _serviceProvider = serviceProvider; private readonly ILogger _logger = logger; @@ -221,7 +226,10 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) try { //create the vectorization request - await vectorizationRequest.UpdateVectorizationRequestResource(vectorizationResourceProvider, DefaultAuthentication.ServiceIdentity!); + await vectorizationRequest.UpdateVectorizationRequestResource( + _instanceSettings.Id, + vectorizationResourceProvider, + DefaultAuthentication.ServiceIdentity!); pipelineState.VectorizationRequestObjectIds.Add(vectorizationRequest.ObjectId!); //issue process action on the created vectorization request await vectorizationRequest.ProcessVectorizationRequest(vectorizationResourceProvider); @@ -318,7 +326,10 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) try { //create the vectorization request - await vectorizationRequest.UpdateVectorizationRequestResource(vectorizationResourceProvider, DefaultAuthentication.ServiceIdentity!); + await vectorizationRequest.UpdateVectorizationRequestResource( + _instanceSettings.Id, + vectorizationResourceProvider, + DefaultAuthentication.ServiceIdentity!); pipelineState.VectorizationRequestObjectIds.Add(vectorizationRequest.ObjectId!); //issue process action on the created vectorization request @@ -328,7 +339,10 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) vectorizationRequest.ProcessingState = VectorizationProcessingState.Failed; pipelineState.ErrorMessages.Add($"Error while submitting process action on vectorization request {vectorizationRequest.Name} in pipeline {pipelineName}: {processResult.ErrorMessage!}"); } - await vectorizationRequest.UpdateVectorizationRequestResource(vectorizationResourceProvider, DefaultAuthentication.ServiceIdentity!); + await vectorizationRequest.UpdateVectorizationRequestResource( + _instanceSettings.Id, + vectorizationResourceProvider, + DefaultAuthentication.ServiceIdentity!); } catch (Exception ex) { @@ -378,7 +392,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) /// The requested resource object. private static async Task GetResource(string objectId, string resourceTypeName, IResourceProviderService resourceProviderService, UnifiedUserIdentity userIdentity) where T : ResourceBase => - await resourceProviderService.GetResource($"/{resourceTypeName}/{objectId.Split("/").Last()}", userIdentity); + await resourceProviderService.GetResourceAsync($"/{resourceTypeName}/{objectId.Split("/").Last()}", userIdentity); private static bool CheckNextExecution(string? cronExpression) diff --git a/src/dotnet/Vectorization/Services/RequestManagerService.cs b/src/dotnet/Vectorization/Services/RequestManagerService.cs index 4f803aeab2..aa9acc7361 100644 --- a/src/dotnet/Vectorization/Services/RequestManagerService.cs +++ b/src/dotnet/Vectorization/Services/RequestManagerService.cs @@ -14,6 +14,7 @@ using FoundationaLLM.Vectorization.ResourceProviders; using FoundationaLLM.Common.Authentication; using FoundationaLLM.Common.Models.Authentication; +using FoundationaLLM.Common.Models.ResourceProviders; namespace FoundationaLLM.Vectorization.Services { @@ -97,17 +98,19 @@ public async Task Run() //hydrate the vectorization request var request = await vectorizationResourceProvider.GetVectorizationRequestResource(dequeuedRequest.RequestName, DefaultAuthentication.ServiceIdentity!); - var processingContext = new VectorizationRequestProcessingContext + var requestInstanceId = default(string); + if (string.IsNullOrWhiteSpace(request.ObjectId) + || !ResourcePath.TryParseInstanceId(request.ObjectId, out requestInstanceId)) { - Request = request, - DequeuedRequest = dequeuedRequest - }; + // We are signaling that the request is invalid by setting the error count to int.MaxValue + request.ErrorCount = int.MaxValue; + } request.ProcessingState = VectorizationProcessingState.InProgress; if (request.ExecutionStart == null) request.ExecutionStart = DateTime.UtcNow; - //check if the dequeue count is greater than the max number of retries + // Check if the request is still valid and the number of retries has not been exceeded. if (request.Expired || request.ErrorCount > _settings.QueueMaxNumberOfRetries) { @@ -122,6 +125,14 @@ public async Task Run() request.LastSuccessfulStepTime); errorMessage = $"The message with id {dequeuedRequest.MessageId} containing the request with id {request.Name} has expired and will be deleted (the last time a step was successfully processed was {request.LastSuccessfulStepTime})."; } + else if (request.ErrorCount.Equals(int.MaxValue)) + { + _logger.LogError( + "The message with id {MessageId} contains the request with id {RequestId} which has an invalid object identifier. The request will be discarded and the message deleted.", + dequeuedRequest.MessageId, + request.Name); + errorMessage = $"The message with id {dequeuedRequest.MessageId} contains the request with id {request.Name} which has an invalid object identifier. The request will be discarded and the message deleted."; + } else { _logger.LogWarning( @@ -158,13 +169,19 @@ public async Task Run() // Persist the state of the vectorization request await _vectorizationStateService.SaveState(state).ConfigureAwait(false); // Update the vectorization request resource - await request.UpdateVectorizationRequestResource(vectorizationResourceProvider, DefaultAuthentication.ServiceIdentity!); + if (!request.ErrorCount.Equals(int.MaxValue)) + await request.UpdateVectorizationRequestResource(requestInstanceId!, vectorizationResourceProvider, DefaultAuthentication.ServiceIdentity!); // Verify if the pipeline state needs to be updated await UpdatePipelineState(request).ConfigureAwait(false); } else { - validRequests.Add(processingContext); + validRequests.Add(new VectorizationRequestProcessingContext + { + InstanceId = requestInstanceId!, + Request = request, + DequeuedRequest = dequeuedRequest + }); } } @@ -187,7 +204,7 @@ public async Task Run() .Select(r => new TaskInfo { PayloadId = r.Request.Name, - Task = ProcessRequest(r.Request, r.DequeuedRequest.MessageId, r.DequeuedRequest.PopReceipt, DefaultAuthentication.ServiceIdentity!, _cancellationToken), + Task = ProcessRequest(r.InstanceId, r.Request, r.DequeuedRequest.MessageId, r.DequeuedRequest.PopReceipt, DefaultAuthentication.ServiceIdentity!, _cancellationToken), StartTime = DateTimeOffset.UtcNow })); @@ -209,7 +226,7 @@ public async Task Run() _logger.LogInformation("The request manager service associated with source [{RequestSourceName}] finished processing requests.", _settings.RequestSourceName); } - private async Task ProcessRequest(VectorizationRequest request, string messageId, string popReceipt, UnifiedUserIdentity userIdentity, CancellationToken cancellationToken) + private async Task ProcessRequest(string requestInstanceId, VectorizationRequest request, string messageId, string popReceipt, UnifiedUserIdentity userIdentity, CancellationToken cancellationToken) { var state = await GetVectorizationRequestState(request); @@ -219,7 +236,7 @@ private async Task ProcessRequest(VectorizationRequest request, string messageId { // If the request was handled successfully, remove it from the current source and advance it to the next step. await _incomingRequestSourceService.DeleteRequest(messageId, popReceipt).ConfigureAwait(false); - await AdvanceRequest(request).ConfigureAwait(false); + await AdvanceRequest(requestInstanceId, request).ConfigureAwait(false); } } catch (Exception ex) @@ -230,7 +247,7 @@ private async Task ProcessRequest(VectorizationRequest request, string messageId finally { await _vectorizationStateService.SaveState(state).ConfigureAwait(false); - await request.UpdateVectorizationRequestResource(GetVectorizationResourceProvider(), DefaultAuthentication.ServiceIdentity!).ConfigureAwait(false); + await request.UpdateVectorizationRequestResource(requestInstanceId, GetVectorizationResourceProvider(), DefaultAuthentication.ServiceIdentity!).ConfigureAwait(false); await UpdatePipelineState(request).ConfigureAwait(false); } @@ -250,7 +267,7 @@ private async Task HandleRequest(VectorizationRequest request, Vectorizati return handlerSuccess; } - private async Task AdvanceRequest(VectorizationRequest request) + private async Task AdvanceRequest(string requestInstanceId, VectorizationRequest request) { var state = await GetVectorizationRequestState(request); @@ -265,7 +282,7 @@ private async Task AdvanceRequest(VectorizationRequest request) var errorMessage = $"Could not find the [{CurrentStep}] request source service for request id {request.Name}."; request.ProcessingState = VectorizationProcessingState.Failed; request.ErrorMessages.Add(errorMessage); - await request.UpdateVectorizationRequestResource(vectorizationResourceProvider, DefaultAuthentication.ServiceIdentity!).ConfigureAwait(false); + await request.UpdateVectorizationRequestResource(requestInstanceId, vectorizationResourceProvider, DefaultAuthentication.ServiceIdentity!).ConfigureAwait(false); throw new VectorizationException(errorMessage); } diff --git a/src/dotnet/Vectorization/Services/RequestProcessors/LocalVectorizationRequestProcessor.cs b/src/dotnet/Vectorization/Services/RequestProcessors/LocalVectorizationRequestProcessor.cs index 6ef8e016ce..e0ed7d8309 100644 --- a/src/dotnet/Vectorization/Services/RequestProcessors/LocalVectorizationRequestProcessor.cs +++ b/src/dotnet/Vectorization/Services/RequestProcessors/LocalVectorizationRequestProcessor.cs @@ -12,10 +12,10 @@ namespace FoundationaLLM.Vectorization.Services.RequestProcessors public class LocalVectorizationRequestProcessor (VectorizationServiceFactory vectorizationServiceFactory) : IVectorizationRequestProcessor { /// - public async Task ProcessRequest(VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity) + public async Task ProcessRequest(string instanceId, VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity) { var vectorizationService = vectorizationServiceFactory!.GetService(vectorizationRequest); - var response = await vectorizationService.ProcessRequest(vectorizationRequest, userIdentity); + var response = await vectorizationService.ProcessRequest(instanceId, vectorizationRequest, userIdentity); return response; } } diff --git a/src/dotnet/Vectorization/Services/RequestProcessors/RemoteVectorizationRequestProcessor.cs b/src/dotnet/Vectorization/Services/RequestProcessors/RemoteVectorizationRequestProcessor.cs index db4b4cb7ed..87e8cfdcd6 100644 --- a/src/dotnet/Vectorization/Services/RequestProcessors/RemoteVectorizationRequestProcessor.cs +++ b/src/dotnet/Vectorization/Services/RequestProcessors/RemoteVectorizationRequestProcessor.cs @@ -17,12 +17,12 @@ public class RemoteVectorizationRequestProcessor( ILoggerFactory loggerFactory) : IVectorizationRequestProcessor { /// - public async Task ProcessRequest(VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity) + public async Task ProcessRequest(string instanceId, VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity) { var vectorizationServiceClient = new VectorizationServiceClient( httpClientFactoryService, loggerFactory.CreateLogger()); - return await vectorizationServiceClient.ProcessRequest(vectorizationRequest, userIdentity); + return await vectorizationServiceClient.ProcessRequest(instanceId, vectorizationRequest, userIdentity); } } } diff --git a/src/dotnet/Vectorization/Services/Text/IndexingServiceFactory.cs b/src/dotnet/Vectorization/Services/Text/IndexingServiceFactory.cs index 5511ac1da6..1493ea5bfc 100644 --- a/src/dotnet/Vectorization/Services/Text/IndexingServiceFactory.cs +++ b/src/dotnet/Vectorization/Services/Text/IndexingServiceFactory.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace FoundationaLLM.Vectorization.Services.Text { @@ -44,7 +45,7 @@ public async Task GetService(string serviceName, UnifiedUserId if (vectorizationResourceProviderService == null) throw new VectorizationException($"The resource provider {ResourceProviderNames.FoundationaLLM_DataSource} was not loaded."); - var indexingProfile = await vectorizationResourceProviderService.GetResource( + var indexingProfile = await vectorizationResourceProviderService.GetResourceAsync( $"/{VectorizationResourceTypeNames.IndexingProfiles}/{serviceName}", userIdentity); return indexingProfile.Indexer switch @@ -61,7 +62,7 @@ public async Task GetService(string serviceName, UnifiedUserId if (vectorizationResourceProviderService == null) throw new VectorizationException($"The resource provider {ResourceProviderNames.FoundationaLLM_Vectorization} was not loaded."); - var indexingProfile = await vectorizationResourceProviderService.GetResource( + var indexingProfile = await vectorizationResourceProviderService.GetResourceAsync( $"/{VectorizationResourceTypeNames.IndexingProfiles}/{serviceName}", userIdentity); return indexingProfile.Indexer switch @@ -86,7 +87,7 @@ private async Task CreateAzureAISearchIndexingService(Indexing if (profile.Settings.TryGetValue("api_endpoint_configuration_object_id", out var apiEndpointObjectId) == false) throw new VectorizationException($"The API endpoint configuration object ID was not found in the settings."); - var apiEndpoint = await configurationResourceProviderService.GetResource(apiEndpointObjectId, DefaultAuthentication.ServiceIdentity!); + var apiEndpoint = await configurationResourceProviderService.GetResourceAsync(apiEndpointObjectId, DefaultAuthentication.ServiceIdentity!); if(apiEndpoint==null) throw new VectorizationException($"The API endpoint configuration {apiEndpointObjectId} for the Azure AI Search service was not found."); diff --git a/src/dotnet/Vectorization/Services/Text/TextEmbeddingServiceFactory.cs b/src/dotnet/Vectorization/Services/Text/TextEmbeddingServiceFactory.cs index a881488f39..92c5007ce8 100644 --- a/src/dotnet/Vectorization/Services/Text/TextEmbeddingServiceFactory.cs +++ b/src/dotnet/Vectorization/Services/Text/TextEmbeddingServiceFactory.cs @@ -40,7 +40,7 @@ public async Task GetService(string serviceName, UnifiedU if (vectorizationResourceProviderService == null) throw new VectorizationException($"The resource provider {ResourceProviderNames.FoundationaLLM_DataSource} was not loaded."); - var textEmbeddingProfile = await vectorizationResourceProviderService.GetResource( + var textEmbeddingProfile = await vectorizationResourceProviderService.GetResourceAsync( $"/{VectorizationResourceTypeNames.TextEmbeddingProfiles}/{serviceName}", userIdentity); return textEmbeddingProfile.TextEmbedding switch @@ -57,7 +57,7 @@ public async Task GetService(string serviceName, UnifiedU if (vectorizationResourceProviderService == null) throw new VectorizationException($"The resource provider {ResourceProviderNames.FoundationaLLM_DataSource} was not loaded."); - var textEmbeddingProfile = await vectorizationResourceProviderService.GetResource( + var textEmbeddingProfile = await vectorizationResourceProviderService.GetResourceAsync( $"/{VectorizationResourceTypeNames.TextEmbeddingProfiles}/{serviceName}", userIdentity); return textEmbeddingProfile.TextEmbedding switch diff --git a/src/dotnet/Vectorization/Services/Text/TextSplitterServiceFactory.cs b/src/dotnet/Vectorization/Services/Text/TextSplitterServiceFactory.cs index 935ef17cd6..776a2e3a78 100644 --- a/src/dotnet/Vectorization/Services/Text/TextSplitterServiceFactory.cs +++ b/src/dotnet/Vectorization/Services/Text/TextSplitterServiceFactory.cs @@ -42,7 +42,7 @@ public async Task GetService(string serviceName, UnifiedUs if (vectorizationResourceProviderService == null) throw new VectorizationException($"The resource provider {ResourceProviderNames.FoundationaLLM_DataSource} was not loaded."); - var textPartitionProfile = await vectorizationResourceProviderService.GetResource( + var textPartitionProfile = await vectorizationResourceProviderService.GetResourceAsync( $"/{VectorizationResourceTypeNames.TextPartitioningProfiles}/{serviceName}", userIdentity); return textPartitionProfile.TextSplitter switch @@ -60,7 +60,7 @@ public async Task GetService(string serviceName, UnifiedUs if (vectorizationResourceProviderService == null) throw new VectorizationException($"The resource provider {ResourceProviderNames.FoundationaLLM_DataSource} was not loaded."); - var textPartitionProfile = await vectorizationResourceProviderService.GetResource( + var textPartitionProfile = await vectorizationResourceProviderService.GetResourceAsync( $"/{VectorizationResourceTypeNames.TextPartitioningProfiles}/{serviceName}", userIdentity); return textPartitionProfile.TextSplitter switch diff --git a/src/dotnet/Vectorization/Services/VectorizationServices/AsynchronousVectorizationService.cs b/src/dotnet/Vectorization/Services/VectorizationServices/AsynchronousVectorizationService.cs index 2cced89b45..0d3e951298 100644 --- a/src/dotnet/Vectorization/Services/VectorizationServices/AsynchronousVectorizationService.cs +++ b/src/dotnet/Vectorization/Services/VectorizationServices/AsynchronousVectorizationService.cs @@ -17,7 +17,7 @@ public class AsynchronousVectorizationService( private readonly Dictionary _requestSources = requestSourcesCache.RequestSources; /// - public async Task ProcessRequest(VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity) + public async Task ProcessRequest(string instanceId, VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity) { var firstRequestSource = _requestSources[vectorizationRequest.Steps.First().Id]; await firstRequestSource.SubmitRequest(vectorizationRequest.Name); diff --git a/src/dotnet/Vectorization/Services/VectorizationServices/SynchronousVectorizationService.cs b/src/dotnet/Vectorization/Services/VectorizationServices/SynchronousVectorizationService.cs index db98ca2945..e08f3464b8 100644 --- a/src/dotnet/Vectorization/Services/VectorizationServices/SynchronousVectorizationService.cs +++ b/src/dotnet/Vectorization/Services/VectorizationServices/SynchronousVectorizationService.cs @@ -40,12 +40,12 @@ public class SynchronousVectorizationService( private readonly ILogger _logger = loggerFactory.CreateLogger(); /// - public async Task ProcessRequest(VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity) + public async Task ProcessRequest(string instanceId, VectorizationRequest vectorizationRequest, UnifiedUserIdentity? userIdentity) { var vectorizationResourceProvider = GetVectorizationResourceProvider(); vectorizationRequest.ProcessingState = VectorizationProcessingState.InProgress; vectorizationRequest.ExecutionStart = DateTime.UtcNow; - await vectorizationRequest.UpdateVectorizationRequestResource(vectorizationResourceProvider, userIdentity!).ConfigureAwait(false); + await vectorizationRequest.UpdateVectorizationRequestResource(instanceId, vectorizationResourceProvider, userIdentity!).ConfigureAwait(false); _logger.LogInformation("Starting synchronous processing for request {RequestId}.", vectorizationRequest.Name); @@ -102,7 +102,7 @@ public async Task ProcessRequest(VectorizationRequest vecto // update the vectorization request state to Completed. vectorizationRequest.ProcessingState = VectorizationProcessingState.Completed; vectorizationRequest.ExecutionEnd = DateTime.UtcNow; - await vectorizationRequest.UpdateVectorizationRequestResource(vectorizationResourceProvider, userIdentity!).ConfigureAwait(false); + await vectorizationRequest.UpdateVectorizationRequestResource(instanceId, vectorizationResourceProvider, userIdentity!).ConfigureAwait(false); _logger.LogInformation("Finished synchronous processing for request {RequestId}. All steps were processed successfully.", vectorizationRequest.Name); return new VectorizationResult(vectorizationRequest.ObjectId!, true, null); @@ -115,7 +115,7 @@ public async Task ProcessRequest(VectorizationRequest vecto // update the vectorization request state to Completed. vectorizationRequest.ProcessingState = VectorizationProcessingState.Failed; vectorizationRequest.ExecutionEnd = DateTime.UtcNow; - await vectorizationRequest.UpdateVectorizationRequestResource(vectorizationResourceProvider, userIdentity!).ConfigureAwait(false); + await vectorizationRequest.UpdateVectorizationRequestResource(instanceId, vectorizationResourceProvider, userIdentity!).ConfigureAwait(false); _logger.LogInformation("Finished synchronous processing for request {RequestId}. {ErrorMessage}", vectorizationRequest.Name, errorMessage); return new VectorizationResult(vectorizationRequest.ObjectId!, false, errorMessage); } diff --git a/src/dotnet/VectorizationAPI/Controllers/VectorizationRequestController.cs b/src/dotnet/VectorizationAPI/Controllers/VectorizationRequestController.cs index e31f2b4147..8e0aadfd82 100644 --- a/src/dotnet/VectorizationAPI/Controllers/VectorizationRequestController.cs +++ b/src/dotnet/VectorizationAPI/Controllers/VectorizationRequestController.cs @@ -1,5 +1,4 @@ using FoundationaLLM.Common.Authentication; -using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.ResourceProviders.Vectorization; using FoundationaLLM.Vectorization.Interfaces; using Microsoft.AspNetCore.Mvc; @@ -10,25 +9,24 @@ namespace FoundationaLLM.Vectorization.API.Controllers /// Methods for managing vectorization requests. /// /// The vectorization request processor. - /// Stores context information extracted from the current HTTP request. This information - /// is primarily used to inject HTTP headers into downstream HTTP calls. /// /// Constructor for the vectorization request controller. /// [ApiController] [APIKeyAuthentication] - [Route("[controller]")] + [Route("instances/{instanceId}")] public class VectorizationRequestController( IVectorizationRequestProcessor vectorizationRequestProcessor) : ControllerBase { /// /// Handles an incoming vectorization request by starting a new vectorization pipeline. /// - /// + /// The FoundationaLLM instance id. + /// The that must be processed. /// - [HttpPost] - public async Task ProcessRequest([FromBody] VectorizationRequest vectorizationRequest) - => new OkObjectResult(await vectorizationRequestProcessor.ProcessRequest(vectorizationRequest, DefaultAuthentication.ServiceIdentity)); + [HttpPost("vectorization-requests")] + public async Task ProcessRequest(string instanceId, [FromBody] VectorizationRequest vectorizationRequest) + => new OkObjectResult(await vectorizationRequestProcessor.ProcessRequest(instanceId, vectorizationRequest, DefaultAuthentication.ServiceIdentity)); } } diff --git a/src/ui/UserPortal/plugins/fileIconPlugin.ts b/src/ui/UserPortal/plugins/fileIconPlugin.ts index f40c602ecf..b1ca33ee59 100644 --- a/src/ui/UserPortal/plugins/fileIconPlugin.ts +++ b/src/ui/UserPortal/plugins/fileIconPlugin.ts @@ -6,9 +6,9 @@ export default defineNuxtPlugin(() => { const getFileIconClass = (fileName: string, useColorVersion: boolean = false) => { let iconClass; if (useColorVersion) { - iconClass = getClassWithColor(fileName.toLowerCase()); + iconClass = getClassWithColor(fileName?.toLowerCase()); } else { - iconClass = getClass(fileName.toLowerCase()); + iconClass = getClass(fileName?.toLowerCase()); } return iconClass || 'pi pi-file'; // Use default icon if no class found }; diff --git a/tests/dotnet/Common.Tests/Models/Chat/CompletionPromptTests.cs b/tests/dotnet/Common.Tests/Models/Chat/CompletionPromptTests.cs index 16d81bce0d..4d59e26560 100644 --- a/tests/dotnet/Common.Tests/Models/Chat/CompletionPromptTests.cs +++ b/tests/dotnet/Common.Tests/Models/Chat/CompletionPromptTests.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration.Response; namespace FoundationaLLM.Common.Tests.Models.Chat diff --git a/tests/dotnet/Common.Tests/Models/Chat/CompletionTests.cs b/tests/dotnet/Common.Tests/Models/Chat/CompletionTests.cs index bd4ca0592e..49ddeaaca6 100644 --- a/tests/dotnet/Common.Tests/Models/Chat/CompletionTests.cs +++ b/tests/dotnet/Common.Tests/Models/Chat/CompletionTests.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; namespace FoundationaLLM.Common.Tests.Models.Chat { diff --git a/tests/dotnet/Common.Tests/Models/Chat/DocumentVectorTests.cs b/tests/dotnet/Common.Tests/Models/Chat/DocumentVectorTests.cs index c0b3327267..dc3c8cc855 100644 --- a/tests/dotnet/Common.Tests/Models/Chat/DocumentVectorTests.cs +++ b/tests/dotnet/Common.Tests/Models/Chat/DocumentVectorTests.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; namespace FoundationaLLM.Common.Tests.Models.Chat { diff --git a/tests/dotnet/Common.Tests/Models/Chat/MessageHistoryItemTests.cs b/tests/dotnet/Common.Tests/Models/Chat/MessageHistoryItemTests.cs index b5d7ce5365..9920ad1863 100644 --- a/tests/dotnet/Common.Tests/Models/Chat/MessageHistoryItemTests.cs +++ b/tests/dotnet/Common.Tests/Models/Chat/MessageHistoryItemTests.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; namespace FoundationaLLM.Common.Tests.Models.Chat { diff --git a/tests/dotnet/Common.Tests/Models/Chat/MessageTests.cs b/tests/dotnet/Common.Tests/Models/Chat/MessageTests.cs index fe35e8ba06..ec0638ac72 100644 --- a/tests/dotnet/Common.Tests/Models/Chat/MessageTests.cs +++ b/tests/dotnet/Common.Tests/Models/Chat/MessageTests.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; namespace FoundationaLLM.Common.Tests.Models.Chat { diff --git a/tests/dotnet/Common.Tests/Models/Chat/SessionTests.cs b/tests/dotnet/Common.Tests/Models/Chat/SessionTests.cs index db4c6e4da7..92e24ed796 100644 --- a/tests/dotnet/Common.Tests/Models/Chat/SessionTests.cs +++ b/tests/dotnet/Common.Tests/Models/Chat/SessionTests.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; namespace FoundationaLLM.Common.Tests.Models.Chat { @@ -7,10 +7,10 @@ public class SessionTests [Fact] public void Constructor_ShouldInitializeProperties() { - var session = new Session(); + var session = new Conversation { Name = "Test" }; Assert.NotNull(session.Id); - Assert.Equal(nameof(Session), session.Type); + Assert.Equal(nameof(Conversation), session.Type); Assert.Equal(session.Id, session.SessionId); Assert.Equal(0, session.TokensUsed); Assert.Equal("New Chat", session.Name); @@ -22,7 +22,7 @@ public void Constructor_ShouldInitializeProperties() public void AddMessage_ShouldAddMessageToMessagesList() { // Arrange - var session = new Session(); + var session = new Conversation { Name = "Test" }; var message = new Message("1", "sender1", null, "The message", null, null, "test@foundationallm.ai"); // Act @@ -36,7 +36,7 @@ public void AddMessage_ShouldAddMessageToMessagesList() public void UpdateMessage_ShouldUpdateExistingMessageInMessagesList() { // Arrange - var session = new Session(); + var session = new Conversation { Name = "Test" }; var initialMessage = new Message("1", "sender1", null, "The message", null, null, "test@foundationallm.ai"); session.AddMessage(initialMessage); diff --git a/tests/dotnet/Common.Tests/Models/Orchestration/CompletionRequestTests.cs b/tests/dotnet/Common.Tests/Models/Orchestration/CompletionRequestTests.cs index f245ef15d9..2f8e8f6039 100644 --- a/tests/dotnet/Common.Tests/Models/Orchestration/CompletionRequestTests.cs +++ b/tests/dotnet/Common.Tests/Models/Orchestration/CompletionRequestTests.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration.Request; namespace FoundationaLLM.Common.Tests.Models.Orchestration diff --git a/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceNameCheckResultTests.cs b/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceNameCheckResultTests.cs index 1e5b6733fe..1899899905 100644 --- a/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceNameCheckResultTests.cs +++ b/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceNameCheckResultTests.cs @@ -19,7 +19,9 @@ public void ResourceNameCheckResult_Properties_Test() Name = expectedName, Type = expectedType, Status = expectedStatus, - Message = expectedMessage + Message = expectedMessage, + Exists = false, + Deleted = false }; // Assert diff --git a/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourcePathTests.cs b/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourcePathTests.cs index 04496c698e..ed59d1813a 100644 --- a/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourcePathTests.cs +++ b/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourcePathTests.cs @@ -1,4 +1,5 @@ -using FoundationaLLM.Common.Exceptions; +using FoundationaLLM.Common.Constants.Authorization; +using FoundationaLLM.Common.Exceptions; using FoundationaLLM.Common.Models.ResourceProviders; using System.Collections.Immutable; @@ -14,10 +15,10 @@ public class ResourcePathTestData : TheoryData, D { { "shapes", - new ResourceTypeDescriptor("shapes") + new ResourceTypeDescriptor("shapes", typeof(object)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], []) + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], []) ], Actions = [ new ResourceTypeAction("action1", false, true, []), @@ -27,10 +28,10 @@ public class ResourcePathTestData : TheoryData, D { { "components", - new ResourceTypeDescriptor("components") + new ResourceTypeDescriptor("components", typeof(object)) { AllowedTypes = [ - new ResourceTypeAllowedTypes(HttpMethod.Get.Method, [], [], []) + new ResourceTypeAllowedTypes(HttpMethod.Get.Method, AuthorizableOperations.Read, [], [], []) ], Actions = [ new ResourceTypeAction("action3", false, true, []), diff --git a/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceProviderUpsertResultTests.cs b/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceProviderUpsertResultTests.cs index 3a0286c94d..9034a4b97b 100644 --- a/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceProviderUpsertResultTests.cs +++ b/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceProviderUpsertResultTests.cs @@ -13,7 +13,8 @@ public void ResourceProviderUpsertResult_ObjectId_Set_Test() // Act var result = new ResourceProviderUpsertResult { - ObjectId = expectedObjectId + ObjectId = expectedObjectId, + ResourceExists = false }; // Assert diff --git a/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceTypeDescriptorTests.cs b/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceTypeDescriptorTests.cs index b0ee7f9fd8..b17847f577 100644 --- a/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceTypeDescriptorTests.cs +++ b/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceTypeDescriptorTests.cs @@ -1,4 +1,5 @@ -using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Constants.Authorization; +using FoundationaLLM.Common.Models.ResourceProviders; namespace FoundationaLLM.Common.Tests.Models.ResourceProvider { @@ -11,10 +12,10 @@ public void ResourceTypeDescriptor_InitializeProperty() string expectedResourceType = "ResourceType"; // Act - var descriptor = new ResourceTypeDescriptor(expectedResourceType); + var descriptor = new ResourceTypeDescriptor(expectedResourceType, typeof(object)); // Assert - Assert.Equal(expectedResourceType, descriptor.ResourceType); + Assert.Equal(expectedResourceType, descriptor.ResourceTypeName); Assert.NotNull(descriptor.Actions); Assert.NotNull(descriptor.AllowedTypes); Assert.NotNull(descriptor.SubTypes); @@ -56,7 +57,7 @@ public void ResourceTypeAllowedTypes_Initialize_CheckProperties() var allowedReturnTypes = new List(); // Act - var allowedTypes = new ResourceTypeAllowedTypes(expectedHttpMethod, allowedParameterTypes, + var allowedTypes = new ResourceTypeAllowedTypes(expectedHttpMethod, AuthorizableOperations.Read, allowedParameterTypes, allowedBodyTypes, allowedReturnTypes); // Assert diff --git a/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceTypeInstanceTests.cs b/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceTypeInstanceTests.cs index 8c56c4719d..534f65dbf0 100644 --- a/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceTypeInstanceTests.cs +++ b/tests/dotnet/Common.Tests/Models/ResourceProvider/ResourceTypeInstanceTests.cs @@ -11,10 +11,10 @@ public void ResourceTypeInstance_InitializeProperty() string expectedResourceType = "ResourceType"; // Act - var instance = new ResourceTypeInstance(expectedResourceType); + var instance = new ResourceTypeInstance(expectedResourceType, typeof(object)); // Assert - Assert.Equal(expectedResourceType, instance.ResourceType); + Assert.Equal(expectedResourceType, instance.ResourceTypeName); Assert.Null(instance.ResourceId); Assert.Null(instance.Action); } diff --git a/tests/dotnet/Core.Client.Tests/CoreClientTests.cs b/tests/dotnet/Core.Client.Tests/CoreClientTests.cs index 265f3a4b61..f7daf3d2b5 100644 --- a/tests/dotnet/Core.Client.Tests/CoreClientTests.cs +++ b/tests/dotnet/Core.Client.Tests/CoreClientTests.cs @@ -1,9 +1,9 @@ using FoundationaLLM.Client.Core.Interfaces; -using FoundationaLLM.Common.Models.Chat; -using FoundationaLLM.Common.Models.ResourceProviders.Agent; +using FoundationaLLM.Common.Models.Conversation; +using FoundationaLLM.Common.Models.Orchestration.Request; using FoundationaLLM.Common.Models.ResourceProviders; +using FoundationaLLM.Common.Models.ResourceProviders.Agent; using NSubstitute; -using FoundationaLLM.Common.Models.Orchestration.Request; namespace FoundationaLLM.Client.Core.Tests { @@ -156,7 +156,6 @@ public async Task GetAgentsAsync_ValidRequest_ReturnsAgents() Name = "TestAgent", Description = "Test Agent Description" }, - Actions = [], Roles = [] } }; diff --git a/tests/dotnet/Core.Examples.LoadTests/Example0001_ResourceProviderResourceReferences.cs b/tests/dotnet/Core.Examples.LoadTests/Example0001_ResourceProviderResourceReferences.cs index 293c55d592..123b08cf07 100644 --- a/tests/dotnet/Core.Examples.LoadTests/Example0001_ResourceProviderResourceReferences.cs +++ b/tests/dotnet/Core.Examples.LoadTests/Example0001_ResourceProviderResourceReferences.cs @@ -81,10 +81,9 @@ private async Task SimulateAssistantUserContextCreation( Prompt = "prompt_placeholder", }; - await resourceProvider.CreateOrUpdateResource( + await resourceProvider.UpsertResourceAsync( instanceId, assistantUserContext, - AzureOpenAIResourceTypeNames.AssistantUserContexts, userIdentity); } diff --git a/tests/dotnet/Core.Examples/Interfaces/IAgentConversationTestService.cs b/tests/dotnet/Core.Examples/Interfaces/IAgentConversationTestService.cs index 78cc27d693..d71a68a7e9 100644 --- a/tests/dotnet/Core.Examples/Interfaces/IAgentConversationTestService.cs +++ b/tests/dotnet/Core.Examples/Interfaces/IAgentConversationTestService.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Core.Examples.Models; namespace FoundationaLLM.Core.Examples.Interfaces; diff --git a/tests/dotnet/Core.Examples/Interfaces/ICoreAPITestManager.cs b/tests/dotnet/Core.Examples/Interfaces/ICoreAPITestManager.cs index 3e1565916c..fb09ced3b0 100644 --- a/tests/dotnet/Core.Examples/Interfaces/ICoreAPITestManager.cs +++ b/tests/dotnet/Core.Examples/Interfaces/ICoreAPITestManager.cs @@ -1,4 +1,4 @@ -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration.Request; namespace FoundationaLLM.Core.Examples.Interfaces; diff --git a/tests/dotnet/Core.Examples/Services/AgentConversationTestService.cs b/tests/dotnet/Core.Examples/Services/AgentConversationTestService.cs index 914402fc28..39d9606aa7 100644 --- a/tests/dotnet/Core.Examples/Services/AgentConversationTestService.cs +++ b/tests/dotnet/Core.Examples/Services/AgentConversationTestService.cs @@ -2,7 +2,7 @@ using FoundationaLLM.Common.Constants; using FoundationaLLM.Common.Interfaces; using FoundationaLLM.Common.Models.AzureAIService; -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration.Request; using FoundationaLLM.Core.Examples.Interfaces; using FoundationaLLM.Core.Examples.Models; diff --git a/tests/dotnet/Core.Examples/Services/CoreAPITestManager.cs b/tests/dotnet/Core.Examples/Services/CoreAPITestManager.cs index 321636396b..c9b21e57a3 100644 --- a/tests/dotnet/Core.Examples/Services/CoreAPITestManager.cs +++ b/tests/dotnet/Core.Examples/Services/CoreAPITestManager.cs @@ -1,5 +1,5 @@ using FoundationaLLM.Common.Constants; -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration.Request; using FoundationaLLM.Common.Settings; using FoundationaLLM.Core.Examples.Exceptions; @@ -24,7 +24,7 @@ public async Task CreateSessionAsync() if (responseSession.IsSuccessStatusCode) { var responseContent = await responseSession.Content.ReadAsStringAsync(); - var sessionResponse = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); + var sessionResponse = JsonSerializer.Deserialize(responseContent, _jsonSerializerOptions); var sessionId = string.Empty; if (sessionResponse?.SessionId != null) { diff --git a/tests/dotnet/Core.Examples/Setup/TestServicesInitializer.cs b/tests/dotnet/Core.Examples/Setup/TestServicesInitializer.cs index 060800ef03..fc1f6ccf2f 100644 --- a/tests/dotnet/Core.Examples/Setup/TestServicesInitializer.cs +++ b/tests/dotnet/Core.Examples/Setup/TestServicesInitializer.cs @@ -6,6 +6,7 @@ using FoundationaLLM.Common.Models.Configuration.CosmosDB; using FoundationaLLM.Common.Models.Configuration.Instance; using FoundationaLLM.Common.Models.Configuration.Storage; +using FoundationaLLM.Common.Services; using FoundationaLLM.Common.Services.Azure; using FoundationaLLM.Common.Services.Storage; using FoundationaLLM.Common.Settings; @@ -13,8 +14,6 @@ using FoundationaLLM.Core.Examples.Interfaces; using FoundationaLLM.Core.Examples.Models; using FoundationaLLM.Core.Examples.Services; -using FoundationaLLM.Core.Interfaces; -using FoundationaLLM.Core.Services; using FoundationaLLM.SemanticKernel.Core.Models.Configuration; using FoundationaLLM.SemanticKernel.Core.Services.Indexing; using Microsoft.Azure.Cosmos; @@ -88,7 +87,7 @@ private static void RegisterCosmosDb(IServiceCollection services, IConfiguration .Build(); }); - services.AddScoped(); + services.AddScoped(); } private static void RegisterAzureAIService(IServiceCollection services, IConfiguration configuration) diff --git a/tests/dotnet/Core.Tests/Services/CoreServiceTests.cs b/tests/dotnet/Core.Tests/Services/CoreServiceTests.cs index a6e34927a8..0a525f8fd0 100644 --- a/tests/dotnet/Core.Tests/Services/CoreServiceTests.cs +++ b/tests/dotnet/Core.Tests/Services/CoreServiceTests.cs @@ -20,7 +20,7 @@ public class CoreServiceTests private readonly string _instanceId = "00000000-0000-0000-0000-000000000000"; private readonly CoreService _testedService; - private readonly ICosmosDbService _cosmosDbService = Substitute.For(); + private readonly ICosmosDBService _cosmosDbService = Substitute.For(); private readonly IGatekeeperAPIService _gatekeeperAPIService = Substitute.For(); private readonly ICallContext _callContext = Substitute.For(); private readonly IEnumerable _resourceProviderServices = Substitute.For>(); diff --git a/tests/dotnet/Gatekeeper.Tests/Services/OrchestrationAPIServiceTests.cs b/tests/dotnet/Gatekeeper.Tests/Services/OrchestrationAPIServiceTests.cs index 7160acc467..4bb197f9fd 100644 --- a/tests/dotnet/Gatekeeper.Tests/Services/OrchestrationAPIServiceTests.cs +++ b/tests/dotnet/Gatekeeper.Tests/Services/OrchestrationAPIServiceTests.cs @@ -1,6 +1,6 @@ using FoundationaLLM.Common.Constants; using FoundationaLLM.Common.Interfaces; -using FoundationaLLM.Common.Models.Chat; +using FoundationaLLM.Common.Models.Conversation; using FoundationaLLM.Common.Models.Orchestration.Request; using FoundationaLLM.Common.Models.Orchestration.Response; using FoundationaLLM.Common.Services.API; diff --git a/tests/dotnet/Management.Client.Tests/Clients/Resources/AIModelManagementClientTests.cs b/tests/dotnet/Management.Client.Tests/Clients/Resources/AIModelManagementClientTests.cs index 455e61bb0b..5c80cd0914 100644 --- a/tests/dotnet/Management.Client.Tests/Clients/Resources/AIModelManagementClientTests.cs +++ b/tests/dotnet/Management.Client.Tests/Clients/Resources/AIModelManagementClientTests.cs @@ -32,7 +32,6 @@ public async Task GetAIModelsAsync_ShouldReturnAIModels() Type = AIModelTypes.Completion, EndpointObjectId = "endpoint-object-id" }, - Actions = [], Roles = [] } }; @@ -69,7 +68,6 @@ public async Task GetAIModelAsync_ShouldReturnAIModel() Type = AIModelTypes.Completion, EndpointObjectId = "endpoint-object-id" }, - Actions = [], Roles = [] }; var expectedAIModels = new List> { expectedAIModel }; @@ -120,7 +118,8 @@ public async Task UpsertAIModel_ShouldReturnUpsertResult() }; var expectedUpsertResult = new ResourceProviderUpsertResult { - ObjectId = "test-object-id" + ObjectId = "test-object-id", + ResourceExists = false }; _mockRestClient.Resources diff --git a/tests/dotnet/Management.Client.Tests/Clients/Resources/AgentManagementClientTests.cs b/tests/dotnet/Management.Client.Tests/Clients/Resources/AgentManagementClientTests.cs index 7c944b2c41..765bdff4ea 100644 --- a/tests/dotnet/Management.Client.Tests/Clients/Resources/AgentManagementClientTests.cs +++ b/tests/dotnet/Management.Client.Tests/Clients/Resources/AgentManagementClientTests.cs @@ -31,7 +31,6 @@ public async Task GetAgentsAsync_ShouldReturnAgents() Description = "A test agent", Type = AgentTypes.KnowledgeManagement }, - Actions = [], Roles = [] } }; @@ -67,7 +66,6 @@ public async Task GetAgentAsync_ShouldReturnAgent() Description = "A test agent", Type = AgentTypes.KnowledgeManagement }, - Actions = [], Roles = [] }; var expectedAgents = new List> { expectedAgent }; @@ -116,7 +114,9 @@ public async Task CheckAgentNameAsync_ShouldReturnCheckResult() { Name = resourceName.Name, Status = NameCheckResultType.Allowed, - Message = "Name is allowed" + Message = "Name is allowed", + Exists = false, + Deleted = false }; _mockRestClient.Resources @@ -193,7 +193,8 @@ public async Task UpsertAgentAsync_ShouldReturnUpsertResult() var agent = new AgentBase { Name = "test-agent" }; var expectedUpsertResult = new ResourceProviderUpsertResult { - ObjectId = "test-object-id" + ObjectId = "test-object-id", + ResourceExists = false }; _mockRestClient.Resources diff --git a/tests/dotnet/Management.Client.Tests/Clients/Resources/AttachmentManagementClientTests.cs b/tests/dotnet/Management.Client.Tests/Clients/Resources/AttachmentManagementClientTests.cs index 50cc7a1944..00f7bd0bdf 100644 --- a/tests/dotnet/Management.Client.Tests/Clients/Resources/AttachmentManagementClientTests.cs +++ b/tests/dotnet/Management.Client.Tests/Clients/Resources/AttachmentManagementClientTests.cs @@ -35,7 +35,6 @@ public async Task GetAttachmentsAsync_ShouldReturnAttachments() Path = "test-attachment.txt", OriginalFileName = "test-attachment.txt" }, - Actions = [], Roles = [] } }; @@ -74,7 +73,6 @@ public async Task GetAttachmentAsync_ShouldReturnAttachment() Path = "test-attachment.txt", OriginalFileName = "test-attachment.txt" }, - Actions = [], Roles = [] }; var expectedAttachments = new List> { expectedAttachment }; @@ -121,7 +119,8 @@ public async Task UpsertAttachmentAsync_ShouldReturnUpsertResult() var attachment = new AttachmentFile { Name = "test-attachment", OriginalFileName = "test-attachment.txt" }; var expectedUpsertResult = new ResourceProviderUpsertResult { - ObjectId = "test-object-id" + ObjectId = "test-object-id", + ResourceExists = false }; ; _mockRestClient.Resources diff --git a/tests/dotnet/Management.Client.Tests/Clients/Resources/ConfigurationManagementClientTests.cs b/tests/dotnet/Management.Client.Tests/Clients/Resources/ConfigurationManagementClientTests.cs index a512db3a81..2a168a36e7 100644 --- a/tests/dotnet/Management.Client.Tests/Clients/Resources/ConfigurationManagementClientTests.cs +++ b/tests/dotnet/Management.Client.Tests/Clients/Resources/ConfigurationManagementClientTests.cs @@ -34,7 +34,6 @@ public async Task GetAppConfigurationsAsync_ShouldReturnConfigurations() Value = "TestValue", ContentType = "text/plain", }, - Actions = [], Roles = [] } }; @@ -73,7 +72,6 @@ public async Task GetAppConfigurationsByFilterAsync_ShouldReturnFilteredConfigur Value = "TestValue", ContentType = "text/plain", }, - Actions = [], Roles = [] } }; @@ -118,7 +116,6 @@ public async Task GetAPIEndpointsAsync_ShouldReturnServices() {AuthenticationParametersKeys.APIKeyHeaderName, "FoundationaLLM:TestAPIKeyHeaderName" } } }, - Actions = [], Roles = [] } }; @@ -162,7 +159,6 @@ public async Task GetAPIEndpointAsync_ShouldReturnService() {AuthenticationParametersKeys.APIKeyHeaderName, "FoundationaLLM:TestAPIKeyHeaderName" } } }, - Actions = [], Roles = [] }; var expectedServices = new List> { expectedService }; @@ -209,7 +205,8 @@ public async Task UpsertAppConfigurationAsync_ShouldReturnUpsertResult() var appConfiguration = new AppConfigurationKeyBase { Name = "test-configuration" }; var expectedUpsertResult = new ResourceProviderUpsertResult { - ObjectId = "test-object-id" + ObjectId = "test-object-id", + ResourceExists = false }; _mockRestClient.Resources @@ -246,7 +243,8 @@ public async Task UpsertAPIEndpointConfiguration_ShouldReturnUpsertResult() }; var expectedUpsertResult = new ResourceProviderUpsertResult { - ObjectId = "test-object-id" + ObjectId = "test-object-id", + ResourceExists = false }; _mockRestClient.Resources diff --git a/tests/dotnet/Management.Client.Tests/Clients/Resources/DataSourceManagementClientTests.cs b/tests/dotnet/Management.Client.Tests/Clients/Resources/DataSourceManagementClientTests.cs index 49ab8ed3e3..c4fd7d4475 100644 --- a/tests/dotnet/Management.Client.Tests/Clients/Resources/DataSourceManagementClientTests.cs +++ b/tests/dotnet/Management.Client.Tests/Clients/Resources/DataSourceManagementClientTests.cs @@ -37,7 +37,6 @@ public async Task GetDataSourcesAsync_ShouldReturnDataSources() }, Folders = ["/folder1", "/folder2"], }, - Actions = [], Roles = [] }, new ResourceProviderGetResult() @@ -53,7 +52,6 @@ public async Task GetDataSourcesAsync_ShouldReturnDataSources() }, Tables = ["Customers", "Orders"], }, - Actions = [], Roles = [] } }; @@ -94,7 +92,6 @@ public async Task GetDataSourceAsync_ShouldReturnDataSource() }, Folders = ["/folder1", "/folder2"], }, - Actions = [], Roles = [] }; var expectedDataSources = new List> { expectedDataSource }; @@ -143,7 +140,9 @@ public async Task CheckDataSourceNameAsync_ShouldReturnCheckResult() { Name = resourceName.Name, Status = NameCheckResultType.Allowed, - Message = "Name is allowed" + Message = "Name is allowed", + Exists = false, + Deleted = false }; _mockRestClient.Resources @@ -219,7 +218,7 @@ public async Task FilterDataSourceAsync_ShouldReturnFilteredDataSources() // Arrange var resourceFilter = new ResourceFilter { - Default = true + DefaultResource = true }; var expectedDataSources = new List { @@ -257,7 +256,8 @@ public async Task UpsertDataSourceAsync_ShouldReturnUpsertResult() var dataSource = new DataSourceBase { Name = "test-dataSource" }; var expectedUpsertResult = new ResourceProviderUpsertResult { - ObjectId = "test-object-id" + ObjectId = "test-object-id", + ResourceExists = false }; _mockRestClient.Resources diff --git a/tests/dotnet/Management.Client.Tests/Clients/Resources/PromptManagementClientTests.cs b/tests/dotnet/Management.Client.Tests/Clients/Resources/PromptManagementClientTests.cs index 6bfe966816..c7a268d7db 100644 --- a/tests/dotnet/Management.Client.Tests/Clients/Resources/PromptManagementClientTests.cs +++ b/tests/dotnet/Management.Client.Tests/Clients/Resources/PromptManagementClientTests.cs @@ -31,7 +31,6 @@ public async Task GetPromptsAsync_ShouldReturnPrompts() Name = "agent-norman", Prefix = "YOu are an analytic agent named Norman. You can answer questions about Norman Rockwell's life and work." }, - Actions = [], Roles = [] }, new ResourceProviderGetResult @@ -41,7 +40,6 @@ public async Task GetPromptsAsync_ShouldReturnPrompts() Name = "agent-bernice", Prefix = "YOu are an analytic agent named Bernice. You can answer questions about all duck breeds and what they eat." }, - Actions = [], Roles = [] } }; @@ -76,7 +74,6 @@ public async Task GetPromptAsync_ShouldReturnPrompt() Name = promptName, Prefix = "YOu are an analytic agent named Bernice. You can answer questions about all duck breeds and what they eat." }, - Actions = [], Roles = [] }; var expectedPrompts = new List> { expectedPrompt }; @@ -125,7 +122,9 @@ public async Task CheckPromptNameAsync_ShouldReturnCheckResult() { Name = resourceName.Name, Status = NameCheckResultType.Allowed, - Message = "Name is allowed" + Message = "Name is allowed", + Exists = false, + Deleted = false }; _mockRestClient.Resources @@ -202,7 +201,8 @@ public async Task UpsertPromptAsync_ShouldReturnUpsertResult() var prompt = new PromptBase { Name = "test-prompt" }; var expectedUpsertResult = new ResourceProviderUpsertResult { - ObjectId = "test-object-id" + ObjectId = "test-object-id", + ResourceExists = false }; _mockRestClient.Resources diff --git a/tests/dotnet/Management.Client.Tests/Clients/Resources/VectorizationManagementClientTests.cs b/tests/dotnet/Management.Client.Tests/Clients/Resources/VectorizationManagementClientTests.cs index 782e6eecb4..48ee5cf315 100644 --- a/tests/dotnet/Management.Client.Tests/Clients/Resources/VectorizationManagementClientTests.cs +++ b/tests/dotnet/Management.Client.Tests/Clients/Resources/VectorizationManagementClientTests.cs @@ -39,7 +39,6 @@ public async Task GetVectorizationPipelinesAsync_ShouldReturnPipelines() TextEmbeddingProfileObjectId = "test-text-embedding-profile", IndexingProfileObjectId = "test-indexing-profile", }, - Actions = [], Roles = [] }, new ResourceProviderGetResult() @@ -54,7 +53,6 @@ public async Task GetVectorizationPipelinesAsync_ShouldReturnPipelines() TextEmbeddingProfileObjectId = "test-text-embedding-profile-2", IndexingProfileObjectId = "test-indexing-profile-2", }, - Actions = [], Roles = [] } }; @@ -94,7 +92,6 @@ public async Task GetVectorizationPipelineAsync_ShouldReturnPipeline() TextEmbeddingProfileObjectId = "test-text-embedding-profile", IndexingProfileObjectId = "test-indexing-profile", }, - Actions = [], Roles = [] }; var expectedPipelines = new List> { expectedPipeline }; @@ -148,7 +145,6 @@ public async Task GetTextPartitioningProfilesAsync_ShouldReturnProfiles() TextSplitter = TextSplitterType.TokenTextSplitter, ObjectId = "test-object-id" }, - Actions = [], Roles = [] }, new ResourceProviderGetResult @@ -159,7 +155,6 @@ public async Task GetTextPartitioningProfilesAsync_ShouldReturnProfiles() TextSplitter = TextSplitterType.TokenTextSplitter, ObjectId = "test-object-id-2" }, - Actions = [], Roles = [] } }; @@ -195,7 +190,6 @@ public async Task GetTextPartitioningProfileAsync_ShouldReturnProfile() TextSplitter = TextSplitterType.TokenTextSplitter, ObjectId = "test-object-id" }, - Actions = [], Roles = [] }; var expectedProfiles = new List> { expectedProfile }; @@ -253,7 +247,6 @@ public async Task GetTextEmbeddingProfilesAsync_ShouldReturnProfiles() { VectorizationSettingsNames.EmbeddingProfileModelName, "text-embedding-ada-002"} } }, - Actions = [], Roles = [] } }; @@ -293,7 +286,6 @@ public async Task GetTextEmbeddingProfileAsync_ShouldReturnProfile() { VectorizationSettingsNames.EmbeddingProfileModelName, "text-embedding-ada-002"} } }, - Actions = [], Roles = [] }; var expectedProfiles = new List> { expectedProfile }; @@ -350,7 +342,6 @@ public async Task GetIndexingProfilesAsync_ShouldReturnProfiles() { VectorizationSettingsNames.IndexingProfileApiEndpointConfigurationObjectId, "test-api-endpoint-object-id" } } }, - Actions = [], Roles = [] }, new ResourceProviderGetResult @@ -364,7 +355,6 @@ public async Task GetIndexingProfilesAsync_ShouldReturnProfiles() { VectorizationSettingsNames.IndexingProfileApiEndpointConfigurationObjectId, "test-api-endpoint-object-id-2" } } }, - Actions = [], Roles = [] }, new ResourceProviderGetResult @@ -378,7 +368,6 @@ public async Task GetIndexingProfilesAsync_ShouldReturnProfiles() { VectorizationSettingsNames.IndexingProfileApiEndpointConfigurationObjectId, "test-api-endpoint-object-id-3" } } }, - Actions = [], Roles = [] } }; @@ -417,7 +406,6 @@ public async Task GetIndexingProfileAsync_ShouldReturnProfile() { VectorizationSettingsNames.IndexingProfileApiEndpointConfigurationObjectId, "test-api-endpoint-object-id" } } }, - Actions = [], Roles = [] }; var expectedProfiles = new List> { expectedProfile }; @@ -655,7 +643,9 @@ public async Task CheckIndexingProfileNameAsync_ShouldReturnCheckResult() { Name = resourceName.Name, Status = NameCheckResultType.Allowed, - Message = "Name is allowed" + Message = "Name is allowed", + Exists = false, + Deleted = false }; _mockRestClient.Resources @@ -694,7 +684,7 @@ public async Task FilterIndexingProfileAsync_ShouldReturnFilteredProfiles() // Arrange var resourceFilter = new ResourceFilter { - Default = false + DefaultResource = false }; var expectedProfiles = new List { @@ -787,7 +777,8 @@ public async Task UpsertVectorizationPipelineAsync_ShouldReturnUpsertResult() var expectedUpsertResult = new ResourceProviderUpsertResult { - ObjectId = "test-object-id" + ObjectId = "test-object-id", + ResourceExists = false }; _mockRestClient.Resources @@ -821,7 +812,8 @@ public async Task UpsertTextPartitioningProfileAsync_ShouldReturnUpsertResult() }; var expectedUpsertResult = new ResourceProviderUpsertResult { - ObjectId = "test-object-id" + ObjectId = "test-object-id", + ResourceExists = false }; _mockRestClient.Resources @@ -859,7 +851,8 @@ public async Task UpsertTextEmbeddingProfileAsync_ShouldReturnUpsertResult() }; var expectedUpsertResult = new ResourceProviderUpsertResult { - ObjectId = "test-object-id" + ObjectId = "test-object-id", + ResourceExists = false }; _mockRestClient.Resources @@ -897,7 +890,8 @@ public async Task UpsertIndexingProfileAsync_ShouldReturnUpsertResult() }; var expectedUpsertResult = new ResourceProviderUpsertResult { - ObjectId = "test-object-id" + ObjectId = "test-object-id", + ResourceExists = false }; _mockRestClient.Resources diff --git a/tests/dotnet/Management.Client.Tests/ManagementClientTests.cs b/tests/dotnet/Management.Client.Tests/ManagementClientTests.cs index 5785bf313f..15bfbf6333 100644 --- a/tests/dotnet/Management.Client.Tests/ManagementClientTests.cs +++ b/tests/dotnet/Management.Client.Tests/ManagementClientTests.cs @@ -51,7 +51,6 @@ public async Task GetResourceByObjectId_ShouldReturnResource() TextEmbeddingProfileObjectId = "test-text-embedding-profile", IndexingProfileObjectId = "test-indexing-profile", }, - Actions = [], Roles = [] }; var expectedResources = new List> { expectedResource }; @@ -103,7 +102,6 @@ public async Task GetResourceWithActionsAndRolesByObjectId_ShouldReturnResourceW TextEmbeddingProfileObjectId = "test-text-embedding-profile", IndexingProfileObjectId = "test-indexing-profile", }, - Actions = [], Roles = [] }; var expectedResources = new List> { expectedResource };