diff --git a/applications/Unity.GrantManager/common.props b/applications/Unity.GrantManager/common.props index e28f59fbc5..85e7cf2d80 100644 --- a/applications/Unity.GrantManager/common.props +++ b/applications/Unity.GrantManager/common.props @@ -20,6 +20,11 @@ + + + \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/index.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/index.md index 768b23b3c2..bc108e3310 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/index.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/index.md @@ -30,17 +30,17 @@ UI-facing AI bits: ## Files ### Application -- `AI/Operations` - validators and helpers -- `AI/Runtime` - rendering, parsing, logging, provider calls -- `AI/Prompts` - prompt types and template plumbing +- `Operations` - validators and helpers +- `Runtime/Execution` - rendering, parsing, logging, provider calls +- `Runtime/Prompts` - prompt types and template plumbing - `DataSeed` - seeded prompt and operation data - `Generation/AIGenerationAppService.cs` - generation API ### Application.Contracts -- `AI/IAIService.cs` - runtime contract +- `IAIService.cs` - runtime contract - `Generation/IAIGenerationAppService.cs` - generation app service contract - `Generation/*ResultDto.cs` - queued result DTOs -- `AI/Operations/IAIGenerationPrerequisiteValidator.cs` - queue prerequisites +- `Operations/IAIGenerationPrerequisiteValidator.cs` - queue prerequisites - `Automation/IApplicationAIGenerationQueue.cs` - queue contract - `Permissions/*` - permissions diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/operation-pipeline.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/operation-pipeline.md new file mode 100644 index 0000000000..e31433e284 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/operation-pipeline.md @@ -0,0 +1,16 @@ +# AI operation pipeline + +AI generation uses a shared operation catalog and queued lifecycle. The catalog owns the operation key, seeded operation name, feature gate, permissions, and whether a form version is required. Submission enters `IAIGenerationAppService.SubmitAsync`, and the Grant Manager queue preserves the existing duplicate-request lock and operation-specific validation. + +The generic background-job base owns tenant scope, structured logging, request state transitions, failure handling, and cooldown stamping. Operation-specific executors remain responsible for loading input, calling the AI contract, validating the response, and persisting the result. + +## Adding an operation + +1. Add an operation definition to `AIGenerationOperations`. +2. Add the operation's submission payload fields only when shared fields are insufficient. +3. Add the AI request/response contract and prompt/operation seed. +4. Implement an `IAIGenerationOperationExecutor` for Grant Manager-specific input and persistence. +5. Register the executor through the existing transient DI convention. +6. Add focused catalog, lifecycle, executor, and persistence tests. + +Do not add another queue branch for shared lifecycle concerns. New operation behavior belongs in its executor; request locking, status transitions, tenant scope, logging, and cooldown behavior stay in the common pipeline. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/docs/prompt-map.md b/applications/Unity.GrantManager/modules/Unity.AI/docs/prompt-map.md index c7d544c1f1..f686766ffc 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/docs/prompt-map.md +++ b/applications/Unity.GrantManager/modules/Unity.AI/docs/prompt-map.md @@ -8,9 +8,16 @@ - `FormWorksheet` - worksheet generation ## Versions -- `v0`, `v1`, `v2` live under `AI/Prompts/Versions` +- `v0`, `v1`, `v2` live under `Runtime/Prompts/Versions` - The seeder loads built-in prompt rows from those versions -- Runtime selects by prompt family and version +- Runtime selects the newest active prompt by family. + +## Tenant selection +- `AIOperation.Name` is the prompt family; operations do not pin a prompt row or version. +- Host requests use the newest active global prompt in the family. +- Tenant requests use the newest active prompt owned by that tenant, falling back to the newest active global prompt. +- To roll back a tenant or global prompt, deactivate the active version and leave the prior version active. +- Tenant prompt rows are administrator-created; deployments seed only global prompts and operations. ## Prompt rules - Versioned prompts are the source of truth. diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Extraction/ITextExtractionService.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Extraction/ITextExtractionService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Extraction/ITextExtractionService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationOperationDefinition.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationOperationDefinition.cs new file mode 100644 index 0000000000..96fee5cabc --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationOperationDefinition.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.AI.Features; +using Unity.AI.Localization; +using Unity.AI.Permissions; + +namespace Unity.AI.Generation; + +public sealed record AIGenerationOperationDefinition( + string OperationType, + string OperationName, + string FeatureName, + string DisabledLocalizationKey, + string GeneratePermission, + string ViewPermission, + bool RequiresFormVersion); + +public static class AIGenerationOperations +{ + public const string AttachmentSummary = "attachment-summary"; + public const string ApplicationAnalysis = "application-analysis"; + public const string ApplicationScoring = "application-scoring"; + public const string FormMapping = "form-mapping"; + public const string FormWorksheet = "form-worksheet"; + public const string FormScoresheet = "form-scoresheet"; + + private static readonly IReadOnlyDictionary Definitions = + new Dictionary(StringComparer.Ordinal) + { + [AttachmentSummary] = new( + AttachmentSummary, + "AttachmentSummary", + AIFeatures.AttachmentSummaries, + AILocalizationKeys.AttachmentSummariesDisabled, + AIPermissions.Analysis.GenerateAttachmentSummaries, + AIPermissions.Analysis.ViewAttachmentSummary, + false), + [ApplicationAnalysis] = new( + ApplicationAnalysis, + "ApplicationAnalysis", + AIFeatures.ApplicationAnalysis, + AILocalizationKeys.ApplicationAnalysisDisabled, + AIPermissions.Analysis.GenerateApplicationAnalysis, + AIPermissions.Analysis.ViewApplicationAnalysis, + false), + [ApplicationScoring] = new( + ApplicationScoring, + "ApplicationScoring", + AIFeatures.Scoring, + AILocalizationKeys.ScoringDisabled, + AIPermissions.Analysis.GenerateScoring, + AIPermissions.Analysis.ViewScoringResult, + false), + [FormMapping] = new( + FormMapping, + "FormMapping", + AIFeatures.FormMapping, + AILocalizationKeys.FormMappingDisabled, + AIPermissions.Analysis.GenerateFormMapping, + AIPermissions.Analysis.ViewFormMapping, + true), + [FormWorksheet] = new( + FormWorksheet, + "FormWorksheet", + AIFeatures.FormWorksheet, + AILocalizationKeys.FormWorksheetDisabled, + AIPermissions.Analysis.GenerateFormWorksheet, + AIPermissions.Analysis.ViewFormWorksheet, + true), + [FormScoresheet] = new( + FormScoresheet, + "FormScoresheet", + AIFeatures.FormScoresheet, + AILocalizationKeys.FormScoresheetDisabled, + AIPermissions.Analysis.GenerateFormScoresheet, + AIPermissions.Analysis.ViewFormScoresheet, + true) + }; + + public static IReadOnlyCollection All => Definitions.Values.ToArray(); + + public static AIGenerationOperationDefinition Get(string operationType) + { + if (!Definitions.TryGetValue(operationType, out var definition)) + { + throw new ArgumentException($"Unsupported AI generation operation type: {operationType}", nameof(operationType)); + } + + return definition; + } + + public static bool TryGet(string? operationType, out AIGenerationOperationDefinition? definition) + { + if (operationType is not null && Definitions.TryGetValue(operationType, out var resolved)) + { + definition = resolved; + return true; + } + + definition = null; + return false; + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationOperationKeyHelper.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationOperationKeyHelper.cs index 5b2a309e8d..42970930b3 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationOperationKeyHelper.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationOperationKeyHelper.cs @@ -2,24 +2,15 @@ namespace Unity.AI.Generation; public static class AIGenerationOperationKeyHelper { - public const string AttachmentSummaryOperationType = "attachment-summary"; - public const string ApplicationAnalysisOperationType = "application-analysis"; - public const string ApplicationScoringOperationType = "application-scoring"; - public const string FormMappingOperationType = "form-mapping"; - public const string FormWorksheetOperationType = "form-worksheet"; - public const string FormScoresheetOperationType = "form-scoresheet"; + public const string AttachmentSummaryOperationType = AIGenerationOperations.AttachmentSummary; + public const string ApplicationAnalysisOperationType = AIGenerationOperations.ApplicationAnalysis; + public const string ApplicationScoringOperationType = AIGenerationOperations.ApplicationScoring; + public const string FormMappingOperationType = AIGenerationOperations.FormMapping; + public const string FormWorksheetOperationType = AIGenerationOperations.FormWorksheet; + public const string FormScoresheetOperationType = AIGenerationOperations.FormScoresheet; public static string? ResolveOperationName(string operationType) - { - return operationType switch - { - ApplicationAnalysisOperationType => "ApplicationAnalysis", - AttachmentSummaryOperationType => "AttachmentSummary", - ApplicationScoringOperationType => "ApplicationScoring", - FormMappingOperationType => "FormMapping", - FormWorksheetOperationType => "FormWorksheet", - FormScoresheetOperationType => "FormScoresheet", - _ => null - }; - } + => AIGenerationOperations.TryGet(operationType, out var definition) + ? definition!.OperationName + : null; } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs new file mode 100644 index 0000000000..537d1702ed --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationRequestDto.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace Unity.AI.Generation; + +public sealed class AIGenerationSubmissionDto +{ + public Guid ApplicationId { get; set; } + + public Guid? ApplicationFormVersionId { get; set; } + + public List AttachmentIds { get; set; } = []; + + public string? PromptVersion { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/IAIGenerationAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/IAIGenerationAppService.cs index 938dff7439..ac94de327d 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/IAIGenerationAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/IAIGenerationAppService.cs @@ -8,6 +8,8 @@ public interface IAIGenerationAppService : IApplicationService { Task GenerateApplicationAttachmentSummariesAsync(AttachmentSummaryGenerationRequestDto request); + Task SubmitAsync(string operationType, AIGenerationSubmissionDto request); + Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null); Task GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/IAIService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/IAIService.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/IAIService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/IAIService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/AIAttachmentItem.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Models/AIAttachmentItem.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/AIAttachmentItem.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Models/AIAttachmentItem.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/AIJsonKeys.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Models/AIJsonKeys.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/AIJsonKeys.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Models/AIJsonKeys.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Models/ApplicationAnalysisFinding.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Models/ApplicationAnalysisFinding.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationScoringAnswer.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Models/ApplicationScoringAnswer.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Models/ApplicationScoringAnswer.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Models/ApplicationScoringAnswer.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/AIApplicationPromptDataDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/AIApplicationPromptDataDto.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/AIApplicationPromptDataDto.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/AIApplicationPromptDataDto.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationAnalysisOperationInputDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/ApplicationAnalysisOperationInputDto.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationAnalysisOperationInputDto.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/ApplicationAnalysisOperationInputDto.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationScoringOperationInputDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/ApplicationScoringOperationInputDto.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/ApplicationScoringOperationInputDto.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/ApplicationScoringOperationInputDto.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIApplicationInputBuilder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/IAIApplicationInputBuilder.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIApplicationInputBuilder.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/IAIApplicationInputBuilder.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIApplicationInputDataProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/IAIApplicationInputDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIApplicationInputDataProvider.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/IAIApplicationInputDataProvider.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIGenerationPrerequisiteValidator.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/IAIGenerationPrerequisiteValidator.cs similarity index 83% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIGenerationPrerequisiteValidator.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/IAIGenerationPrerequisiteValidator.cs index cbbfeae8ea..18e1f42ae9 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAIGenerationPrerequisiteValidator.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/IAIGenerationPrerequisiteValidator.cs @@ -1,10 +1,13 @@ using System; using System.Threading.Tasks; +using Unity.AI.Generation; namespace Unity.AI.Operations; public interface IAIGenerationPrerequisiteValidator { + Task EnsureAvailableAsync(string operationType, AIGenerationSubmissionDto request); + Task EnsureAttachmentSummaryAvailableAsync(Guid applicationId); Task EnsureApplicationAnalysisAvailableAsync(Guid applicationId); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAttachmentSummaryDataProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/IAttachmentSummaryDataProvider.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAttachmentSummaryDataProvider.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/IAttachmentSummaryDataProvider.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Prompts/UnityPromptAssetManifest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/UnityPromptAssetManifest.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Prompts/UnityPromptAssetManifest.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/UnityPromptAssetManifest.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/IAIRateLimiter.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/IAIRateLimiter.cs index 74525aad89..87893505c4 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/IAIRateLimiter.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/RateLimit/IAIRateLimiter.cs @@ -11,6 +11,12 @@ public interface IAIRateLimiter /// Task EnsureAsync(); + /// + /// Throws if the supplied user is still inside their AI generate cooldown window. + /// No-op when userId is null. + /// + Task EnsureAsync(Guid? userId); + /// /// Starts a fresh cooldown for the current user. No-op for callers without a user. /// diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/ApplicationAnalysisRequest.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/ApplicationAnalysisRequest.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/ApplicationScoringRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/ApplicationScoringRequest.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/ApplicationScoringRequest.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/ApplicationScoringRequest.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/AttachmentSummaryRequest.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/AttachmentSummaryRequest.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/FormMappingRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/FormMappingRequest.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/FormMappingRequest.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/FormMappingRequest.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/FormScoresheetRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/FormScoresheetRequest.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/FormScoresheetRequest.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/FormScoresheetRequest.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/FormWorksheetRequest.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/FormWorksheetRequest.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Requests/FormWorksheetRequest.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/FormWorksheetRequest.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/ApplicationAnalysisResponse.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/ApplicationAnalysisResponse.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/ApplicationScoringResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/ApplicationScoringResponse.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/ApplicationScoringResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/ApplicationScoringResponse.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/AttachmentSummaryResponse.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/AttachmentSummaryResponse.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormMappingFieldResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingFieldResponse.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormMappingFieldResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingFieldResponse.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormMappingIssueResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingIssueResponse.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormMappingIssueResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingIssueResponse.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormMappingMatchResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingMatchResponse.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormMappingMatchResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingMatchResponse.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormMappingResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingResponse.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormMappingResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingResponse.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormMappingWorksheetResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingWorksheetResponse.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormMappingWorksheetResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormMappingWorksheetResponse.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormScoresheetResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormScoresheetResponse.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormScoresheetResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormScoresheetResponse.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormWorksheetCreationResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormWorksheetCreationResponse.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormWorksheetCreationResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormWorksheetCreationResponse.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormWorksheetResponse.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormWorksheetResponse.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Responses/FormWorksheetResponse.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Responses/FormWorksheetResponse.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIOperationDataSeeder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIOperationDataSeeder.cs index 748a187327..d4504d69c7 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIOperationDataSeeder.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIOperationDataSeeder.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Unity.AI.Operations; using Unity.AI.Domain; -using Unity.AI.Prompts; +using Unity.AI.Runtime.Prompts; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; @@ -16,7 +16,6 @@ namespace Unity.AI.DataSeed; public class AIOperationDataSeeder( IRepository operationRepository, IRepository modelRepository, - IRepository promptRepository, ICurrentTenant currentTenant, ILogger logger) : ITransientDependency { @@ -24,12 +23,12 @@ public class AIOperationDataSeeder( private static readonly BuiltInOperationDefinition[] BuiltInOperations = [ - new(AIPromptTypes.ApplicationAnalysis, AIPromptTypes.ApplicationAnalysis, 1, 4000), - new(AIPromptTypes.AttachmentSummary, AIPromptTypes.AttachmentSummary, 1, 2000), - new(AIPromptTypes.ApplicationScoring, AIPromptTypes.ApplicationScoring, 1, 8000), - new(AIPromptTypes.FormMapping, AIPromptTypes.FormMapping, 2, 2000), - new(AIPromptTypes.FormWorksheet, AIPromptTypes.FormWorksheet, 2, 4000), - new(AIPromptTypes.FormScoresheet, AIPromptTypes.FormScoresheet, 2, 4000) + new(AIPromptTypes.ApplicationAnalysis, 4000), + new(AIPromptTypes.AttachmentSummary, 2000), + new(AIPromptTypes.ApplicationScoring, 8000), + new(AIPromptTypes.FormMapping, 2000), + new(AIPromptTypes.FormWorksheet, 4000), + new(AIPromptTypes.FormScoresheet, 4000) ]; public async Task SeedAsync(DataSeedContext context) @@ -57,22 +56,10 @@ public async Task SeedAsync(DataSeedContext context) private async Task EnsureOperationAsync(BuiltInOperationDefinition definition, AIModel model) { - var prompt = await ResolvePromptAsync(definition.PromptName, definition.PromptVersionNumber); - if (prompt == null) - { - logger.LogWarning( - "AI operation seeding skipped: no active prompt found for operation '{OperationName}' and prompt '{PromptName}' version '{PromptVersionNumber}'.", - definition.OperationName, - definition.PromptName, - definition.PromptVersionNumber); - return; - } - var existing = await operationRepository.FirstOrDefaultAsync(op => op.Name == definition.OperationName); if (existing != null) { existing.AIModelId = model.Id; - existing.AIPromptId = prompt.Id; existing.ExecutionMode = definition.ExecutionMode; existing.CompletionTokens = definition.CompletionTokens; existing.IsActive = true; @@ -81,7 +68,7 @@ private async Task EnsureOperationAsync(BuiltInOperationDefinition definition, A } await operationRepository.InsertAsync( - new AIOperation(Guid.CreateVersion7(), definition.OperationName, model.Id, prompt.Id) + new AIOperation(Guid.CreateVersion7(), definition.OperationName, model.Id) { ExecutionMode = definition.ExecutionMode, CompletionTokens = definition.CompletionTokens, @@ -96,18 +83,8 @@ await operationRepository.InsertAsync( return models.FirstOrDefault(); } - private async Task ResolvePromptAsync(string promptName, int promptVersionNumber) - { - return await promptRepository.FirstOrDefaultAsync(item => - item.Name == promptName && - item.VersionNumber == promptVersionNumber && - item.IsActive); - } - private sealed record BuiltInOperationDefinition( string OperationName, - string PromptName, - int PromptVersionNumber, int CompletionTokens, AIExecutionMode ExecutionMode = AIExecutionMode.Sequential); } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIPromptDataSeeder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIPromptDataSeeder.cs index 7df54a2a2a..0d1af31c57 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIPromptDataSeeder.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIPromptDataSeeder.cs @@ -3,7 +3,7 @@ using System.Text.Json; using System.Threading.Tasks; using Unity.AI.Domain; -using Unity.AI.Prompts; +using Unity.AI.Runtime.Prompts; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; @@ -177,7 +177,7 @@ await promptRepository.InsertAsync(new AIPrompt( } // ═════════════════════════════════════════════════════════════════════════ - // PROMPT CONTENT — mirrors AI/Prompts/Versions/ text files verbatim + // PROMPT CONTENT — mirrors Runtime/Prompts/Versions/ text files verbatim // ═════════════════════════════════════════════════════════════════════════ // ── v0/analysis.system.txt ─────────────────────────────────────────────── diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIOperation.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIOperation.cs index c4aa1f229f..465a5f4824 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIOperation.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIOperation.cs @@ -12,10 +12,6 @@ public class AIOperation : AuditedAggregateRoot public AIModel AIModel { get; set; } = default!; - public Guid AIPromptId { get; set; } - - public AIPrompt? AIPrompt { get; set; } - public AIExecutionMode ExecutionMode { get; set; } = AIExecutionMode.Sequential; public int CompletionTokens { get; set; } @@ -26,11 +22,11 @@ protected AIOperation() { } - public AIOperation(Guid id, string name, Guid aiModelId, Guid aiPromptId) + public AIOperation(Guid id, string name, Guid aiModelId) { Id = id; Name = name; AIModelId = aiModelId; - AIPromptId = aiPromptId; } + } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs index 7184e09c74..99633a9030 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs @@ -94,10 +94,6 @@ public static void ConfigureAI(this ModelBuilder modelBuilder) .HasForeignKey(x => x.AIModelId) .OnDelete(DeleteBehavior.Restrict); - b.HasOne(x => x.AIPrompt) - .WithMany() - .HasForeignKey(x => x.AIPromptId) - .OnDelete(DeleteBehavior.Restrict); }); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Extraction/TextExtractionService.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Extraction/TextExtractionService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Extraction/TextExtractionService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs index c297fef32a..582c7d1cfb 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/AIGenerationAppService.cs @@ -21,79 +21,94 @@ public class AIGenerationAppService( ICurrentTenant currentTenant) : AIAppService, IAIGenerationAppService { - [Authorize(AIPermissions.Analysis.GenerateAttachmentSummaries)] - [HttpPost("attachment-summary")] - public virtual async Task GenerateApplicationAttachmentSummariesAsync(AttachmentSummaryGenerationRequestDto request) + [Authorize] + [HttpPost("submit")] + public virtual async Task SubmitAsync(string operationType, AIGenerationSubmissionDto request) { - await featureGuard.EnsureEnabledAsync( - AIFeatures.AttachmentSummaries, - AILocalizationKeys.AttachmentSummariesDisabled); + var operation = AIGenerationOperations.Get(operationType); + await featureGuard.EnsureEnabledAsync(operation.FeatureName, operation.DisabledLocalizationKey); - if (request.AttachmentIds.Count == 0) + if (operation.RequiresFormVersion && request.ApplicationFormVersionId is null) { - return; + throw new UserFriendlyException($"AI operation '{operationType}' requires an application form version."); } - await aiGenerationQueue.QueueApplicationAttachmentSummaryAsync( - request.ApplicationId, - currentTenant.Id, - request.AttachmentIds, - request.PromptVersion); + await CheckPolicyAsync(operation.GeneratePermission); + await aiGenerationQueue.QueueAsync(operationType, request, currentTenant.Id); + } + + [Authorize(AIPermissions.Analysis.GenerateAttachmentSummaries)] + [HttpPost("attachment-summary")] + public virtual async Task GenerateApplicationAttachmentSummariesAsync(AttachmentSummaryGenerationRequestDto request) + { + await SubmitAsync( + AIGenerationOperations.AttachmentSummary, + new AIGenerationSubmissionDto + { + ApplicationId = request.ApplicationId, + AttachmentIds = request.AttachmentIds, + PromptVersion = request.PromptVersion + }); } [Authorize(AIPermissions.Analysis.GenerateApplicationAnalysis)] [HttpPost("application-analysis")] public virtual async Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null) { - await featureGuard.EnsureEnabledAsync( - AIFeatures.ApplicationAnalysis, - AILocalizationKeys.ApplicationAnalysisDisabled); - - await aiGenerationQueue.QueueApplicationAnalysisAsync(applicationId, currentTenant.Id, promptVersion); + await SubmitAsync( + AIGenerationOperations.ApplicationAnalysis, + new AIGenerationSubmissionDto { ApplicationId = applicationId, PromptVersion = promptVersion }); } [Authorize(AIPermissions.Analysis.GenerateScoring)] [HttpPost("application-scoring")] public virtual async Task GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null) { - await featureGuard.EnsureEnabledAsync( - AIFeatures.Scoring, - AILocalizationKeys.ScoringDisabled); - - await aiGenerationQueue.QueueApplicationScoringAsync(applicationId, currentTenant.Id, promptVersion); + await SubmitAsync( + AIGenerationOperations.ApplicationScoring, + new AIGenerationSubmissionDto { ApplicationId = applicationId, PromptVersion = promptVersion }); } [Authorize(AIPermissions.Analysis.GenerateFormMapping)] [HttpPost("form-mapping")] public virtual async Task GenerateFormMappingAsync(Guid applicationId, Guid applicationFormVersionId, string? promptVersion = null) { - await featureGuard.EnsureEnabledAsync( - AIFeatures.FormMapping, - AILocalizationKeys.FormMappingDisabled); - - await aiGenerationQueue.QueueFormMappingAsync(applicationId, currentTenant.Id, applicationFormVersionId, promptVersion); + await SubmitAsync( + AIGenerationOperations.FormMapping, + new AIGenerationSubmissionDto + { + ApplicationId = applicationId, + ApplicationFormVersionId = applicationFormVersionId, + PromptVersion = promptVersion + }); } [Authorize(AIPermissions.Analysis.GenerateFormWorksheet)] [HttpPost("form-worksheet")] public virtual async Task GenerateFormWorksheetAsync(Guid applicationId, Guid applicationFormVersionId, string? promptVersion = null) { - await featureGuard.EnsureEnabledAsync( - AIFeatures.FormWorksheet, - AILocalizationKeys.FormWorksheetDisabled); - - await aiGenerationQueue.QueueFormWorksheetAsync(applicationId, currentTenant.Id, applicationFormVersionId, promptVersion); + await SubmitAsync( + AIGenerationOperations.FormWorksheet, + new AIGenerationSubmissionDto + { + ApplicationId = applicationId, + ApplicationFormVersionId = applicationFormVersionId, + PromptVersion = promptVersion + }); } [Authorize(AIPermissions.Analysis.GenerateFormScoresheet)] [HttpPost("form-scoresheet")] public virtual async Task GenerateFormScoresheetAsync(Guid applicationId, Guid applicationFormVersionId, string? promptVersion = null) { - await featureGuard.EnsureEnabledAsync( - AIFeatures.FormScoresheet, - AILocalizationKeys.FormScoresheetDisabled); - - await aiGenerationQueue.QueueFormScoresheetAsync(applicationId, currentTenant.Id, applicationFormVersionId, promptVersion); + await SubmitAsync( + AIGenerationOperations.FormScoresheet, + new AIGenerationSubmissionDto + { + ApplicationId = applicationId, + ApplicationFormVersionId = applicationFormVersionId, + PromptVersion = promptVersion + }); } [Authorize] @@ -136,17 +151,11 @@ public virtual async Task GetStatusAsync(Guid application private async Task EnsureStatusAccessAsync(string operationType) { - var permission = operationType switch + if (!AIGenerationOperations.TryGet(operationType, out var operation)) { - AIGenerationOperationKeyHelper.ApplicationAnalysisOperationType => AIPermissions.Analysis.ViewApplicationAnalysis, - AIGenerationOperationKeyHelper.AttachmentSummaryOperationType => AIPermissions.Analysis.ViewAttachmentSummary, - AIGenerationOperationKeyHelper.ApplicationScoringOperationType => AIPermissions.Analysis.ViewScoringResult, - AIGenerationOperationKeyHelper.FormMappingOperationType => AIPermissions.Analysis.ViewFormMapping, - AIGenerationOperationKeyHelper.FormWorksheetOperationType => AIPermissions.Analysis.ViewFormWorksheet, - AIGenerationOperationKeyHelper.FormScoresheetOperationType => AIPermissions.Analysis.ViewFormScoresheet, - _ => throw new UserFriendlyException($"Unsupported AI generation operation type: {operationType}") - }; + throw new UserFriendlyException($"Unsupported AI generation operation type: {operationType}"); + } - await CheckPolicyAsync(permission); + await CheckPolicyAsync(operation!.ViewPermission); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/IApplicationGenerationQueue.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/IApplicationGenerationQueue.cs index ac57e353b1..9b4e400a9d 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/IApplicationGenerationQueue.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Generation/IApplicationGenerationQueue.cs @@ -1,22 +1,11 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; namespace Unity.AI.Generation; public interface IApplicationGenerationQueue { - Task QueueApplicationAttachmentSummaryAsync(Guid applicationId, Guid? tenantId, List attachmentIds, string? promptVersion = null); - - Task QueueApplicationAnalysisAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); - - Task QueueApplicationScoringAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); - - Task QueueFormMappingAsync(Guid applicationId, Guid? tenantId, Guid applicationFormVersionId, string? promptVersion = null); - - Task QueueFormWorksheetAsync(Guid applicationId, Guid? tenantId, Guid applicationFormVersionId, string? promptVersion = null); - - Task QueueFormScoresheetAsync(Guid applicationId, Guid? tenantId, Guid applicationFormVersionId, string? promptVersion = null); + Task QueueAsync(string operationType, AIGenerationSubmissionDto request, Guid? tenantId); Task QueueApplicationIntakeAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null); } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIApplicationInputBuilder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIApplicationInputBuilder.cs similarity index 99% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIApplicationInputBuilder.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIApplicationInputBuilder.cs index 1378b5bbd5..9e3dd34802 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIApplicationInputBuilder.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIApplicationInputBuilder.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Threading.Tasks; using Unity.AI.Models; -using Unity.AI.Prompts; +using Unity.AI.Runtime.Prompts; using Volo.Abp; using Volo.Abp.DependencyInjection; diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIExecutionMode.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIExecutionMode.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIExecutionMode.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIExecutionMode.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIExecutionModeResolver.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIExecutionModeResolver.cs similarity index 98% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIExecutionModeResolver.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIExecutionModeResolver.cs index 5408e2eade..e988120367 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIExecutionModeResolver.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIExecutionModeResolver.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Configuration; using System; -using Unity.AI.Prompts; +using Unity.AI.Runtime.Prompts; using Volo.Abp.DependencyInjection; namespace Unity.AI.Operations; diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIExecutionStrategy.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIExecutionStrategy.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIExecutionStrategy.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AIExecutionStrategy.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/ApplicationAnalysisService.cs similarity index 97% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/ApplicationAnalysisService.cs index b2ce118946..42905d2f96 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/ApplicationAnalysisService.cs @@ -4,7 +4,7 @@ using Unity.AI; using Unity.AI.Models; using Unity.AI.Requests; -using Unity.AI.Runtime; +using Unity.AI.Runtime.Execution; using Volo.Abp.DependencyInjection; namespace Unity.AI.Operations diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/ApplicationScoringService.cs similarity index 99% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/ApplicationScoringService.cs index ec6bb55e8d..aeb4dadb1e 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/ApplicationScoringService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/ApplicationScoringService.cs @@ -8,7 +8,7 @@ using Unity.AI; using Unity.AI.Models; using Unity.AI.Requests; -using Unity.AI.Runtime; +using Unity.AI.Runtime.Execution; using Volo.Abp.DependencyInjection; namespace Unity.AI.Operations diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AttachmentSummaryService.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/AttachmentSummaryService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IApplicationAnalysisService.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationAnalysisService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IApplicationAnalysisService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IApplicationScoringService.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IApplicationScoringService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IApplicationScoringService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IAttachmentSummaryService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IAttachmentSummaryService.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IAttachmentSummaryService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IAttachmentSummaryService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IFormMappingService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IFormMappingService.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IFormMappingService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IFormMappingService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IFormScoresheetService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IFormScoresheetService.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IFormScoresheetService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IFormScoresheetService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IFormWorksheetService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IFormWorksheetService.cs similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/IFormWorksheetService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Operations/IFormWorksheetService.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Prompts/AIPromptAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Prompts/AIPromptAppService.cs index 0d6c9b02f9..25f8e6d688 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Prompts/AIPromptAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Prompts/AIPromptAppService.cs @@ -6,12 +6,13 @@ using System.Threading.Tasks; using Unity.AI.Domain; using Unity.Modules.Shared.Permissions; -using Volo.Abp.Data; +using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; +using Volo.Abp.Data; using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; -using Volo.Abp; +using Volo.Abp.Authorization; namespace Unity.AI.Prompts; @@ -27,13 +28,16 @@ public class AIPromptAppService : IAIPromptAppService { private readonly IDataFilter _multiTenantDataFilter; + private readonly ICurrentTenant _currentTenant; public AIPromptAppService( IRepository repository, - IDataFilter multiTenantDataFilter) + IDataFilter multiTenantDataFilter, + ICurrentTenant currentTenant) : base(repository) { _multiTenantDataFilter = multiTenantDataFilter; + _currentTenant = currentTenant; GetPolicyName = IdentityConsts.ITOperationsPolicyName; GetListPolicyName = IdentityConsts.ITOperationsPolicyName; CreatePolicyName = IdentityConsts.ITOperationsPolicyName; @@ -47,6 +51,7 @@ public virtual async Task> GetByPromptAsync(Guid prom using (_multiTenantDataFilter.Disable()) { var selected = await Repository.GetAsync(promptId); + EnsureReadAccess(selected); var items = await Repository.GetListAsync(v => v.TenantId == selected.TenantId && v.Name == selected.Name); var sorted = items.OrderBy(v => v.VersionNumber).ToList(); return new ListResultDto( @@ -59,7 +64,9 @@ public override async Task GetAsync(Guid id) { using (_multiTenantDataFilter.Disable()) { - return await base.GetAsync(id); + var prompt = await Repository.GetAsync(id); + EnsureReadAccess(prompt); + return ObjectMapper.Map(prompt); } } @@ -68,7 +75,22 @@ public override async Task> GetListAsync(PagedAndSor { using (_multiTenantDataFilter.Disable()) { - return await base.GetListAsync(input); + var prompts = await Repository.GetListAsync(); + var accessiblePrompts = _currentTenant.Id is Guid tenantId + ? prompts.Where(prompt => prompt.TenantId == null || prompt.TenantId == tenantId) + : prompts; + var sortedPrompts = accessiblePrompts + .OrderBy(prompt => prompt.Name) + .ThenByDescending(prompt => prompt.VersionNumber) + .ToList(); + var items = sortedPrompts + .Skip(input.SkipCount) + .Take(input.MaxResultCount) + .ToList(); + + return new PagedResultDto( + sortedPrompts.Count, + ObjectMapper.Map, List>(items)); } } @@ -78,8 +100,10 @@ public override async Task CreateAsync(CreateUpdateAIPromptDto inpu using (_multiTenantDataFilter.Disable()) { var prompt = await Repository.GetAsync(input.PromptId); + EnsureReadAccess(prompt); + var targetTenantId = _currentTenant.Id ?? prompt.TenantId; var existingVersion = await Repository.FirstOrDefaultAsync(p => - p.TenantId == prompt.TenantId && + p.TenantId == targetTenantId && p.Name == prompt.Name && p.VersionNumber == input.VersionNumber); if (existingVersion != null) @@ -95,7 +119,7 @@ public override async Task CreateAsync(CreateUpdateAIPromptDto inpu input.VersionNumber, input.SystemPrompt, input.UserPrompt, - prompt.TenantId) + targetTenantId) { MetadataJson = string.IsNullOrWhiteSpace(input.MetadataJson) ? "{}" : input.MetadataJson, IsActive = input.IsActive @@ -111,6 +135,7 @@ public override async Task UpdateAsync(Guid id, CreateUpdateAIPromp using (_multiTenantDataFilter.Disable()) { var entity = await Repository.GetAsync(id); + EnsureMutationAccess(entity); var conflictingVersion = await Repository.FirstOrDefaultAsync(p => p.Id != id && p.TenantId == entity.TenantId && @@ -132,11 +157,30 @@ public override async Task UpdateAsync(Guid id, CreateUpdateAIPromp } } + private void EnsureReadAccess(AIPrompt prompt) + { + if (_currentTenant.Id is Guid tenantId && prompt.TenantId is not null && prompt.TenantId != tenantId) + { + throw new AbpAuthorizationException("The selected AI prompt belongs to another tenant."); + } + } + + private void EnsureMutationAccess(AIPrompt prompt) + { + if (_currentTenant.Id is Guid tenantId && prompt.TenantId != tenantId) + { + throw new AbpAuthorizationException("The selected AI prompt cannot be modified from this tenant."); + } + } + + [RemoteService(false)] [HttpDelete("{id}")] public override async Task DeleteAsync(Guid id) { using (_multiTenantDataFilter.Disable()) { + var entity = await Repository.GetAsync(id); + EnsureMutationAccess(entity); await base.DeleteAsync(id); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AICooldownService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AICooldownService.cs deleted file mode 100644 index 136d7d3ae7..0000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AICooldownService.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Globalization; -using System.Threading.Tasks; -using Medallion.Threading; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Configuration; -using Volo.Abp; -using Volo.Abp.DependencyInjection; - -namespace Unity.AI.Cooldown; - -public interface IAICooldownService -{ - Task GetRemainingSecondsAsync(Guid userId); - Task EnsureAsync(Guid? userId); - Task StampAsync(Guid? userId); -} - -public class AICooldownService( - IDistributedCache cache, - IConfiguration configuration, - IDistributedLockProvider distributedLockProvider) - : IAICooldownService, ITransientDependency -{ - public async Task GetRemainingSecondsAsync(Guid userId) - { - var raw = await cache.GetStringAsync(KeyFor(userId)); - if (string.IsNullOrEmpty(raw) || - !long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var untilTicks) || - untilTicks < DateTime.MinValue.Ticks || - untilTicks > DateTime.MaxValue.Ticks) - { - return 0; - } - - var seconds = (int)Math.Ceiling((new DateTime(untilTicks, DateTimeKind.Utc) - DateTime.UtcNow).TotalSeconds); - return seconds > 0 ? seconds : 0; - } - - public async Task EnsureAsync(Guid? userId) - { - if (userId is not Guid resolvedUserId) - { - return; - } - - var userLock = distributedLockProvider.CreateLock(CooldownLockPrefix + resolvedUserId); - using (await userLock.AcquireAsync()) - { - var remaining = await GetRemainingSecondsAsync(resolvedUserId); - if (remaining > 0) - { - throw new UserFriendlyException( - $"AI generation is rate limited. Try again in {remaining} second{(remaining == 1 ? "" : "s")}."); - } - } - } - - public async Task StampAsync(Guid? userId) - { - if (userId is Guid resolvedUserId) - { - var userLock = distributedLockProvider.CreateLock(CooldownLockPrefix + resolvedUserId); - using (await userLock.AcquireAsync()) - { - await StampAsync(resolvedUserId, CooldownSeconds); - } - } - } - - private const string CooldownKeyPrefix = "ai-generation:cooldown:"; - private const string CooldownLockPrefix = "ai-generation:cooldown-lock:"; - private const string CooldownSecondsConfigurationKey = "Azure:Generation:CooldownSeconds"; - - private int CooldownSeconds - { - get - { - var configured = configuration.GetValue(CooldownSecondsConfigurationKey); - if (configured is > 0) - { - return configured.Value; - } - - throw new AbpException($"{CooldownSecondsConfigurationKey} must be configured with a positive value."); - } - } - - private async Task StampAsync(Guid userId, int seconds) - { - var until = DateTime.UtcNow.AddSeconds(seconds); - await cache.SetStringAsync( - KeyFor(userId), - until.Ticks.ToString(CultureInfo.InvariantCulture), - new DistributedCacheEntryOptions { AbsoluteExpiration = until }); - } - - private static string KeyFor(Guid userId) => CooldownKeyPrefix + userId; -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs index fe5a68b569..6b6f1dc8a1 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/RateLimit/AIRateLimiter.cs @@ -42,18 +42,20 @@ private int CooldownSeconds } } - public virtual async Task EnsureAsync() + public virtual Task EnsureAsync() => EnsureAsync(currentUser.Id); + + public virtual async Task EnsureAsync(Guid? userId) { - if (currentUser.Id is not Guid userId) + if (userId is not Guid resolvedUserId) { // No user (background/system flow). User-level rate limit does not apply. return; } - var userLock = distributedLockProvider.CreateLock(CooldownLockPrefix + userId); + var userLock = distributedLockProvider.CreateLock(CooldownLockPrefix + resolvedUserId); using (await userLock.AcquireAsync()) { - var remaining = await GetRemainingSecondsAsync(userId); + var remaining = await GetRemainingSecondsAsync(resolvedUserId); if (remaining > 0) { throw new UserFriendlyException( diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIJsonDefaults.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIJsonDefaults.cs similarity index 93% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIJsonDefaults.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIJsonDefaults.cs index 1b322a5228..479f84701d 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIJsonDefaults.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIJsonDefaults.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; internal static class AIJsonDefaults { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIOperationResult.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIOperationResult.cs similarity index 98% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIOperationResult.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIOperationResult.cs index efd964c51d..41b3989649 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIOperationResult.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIOperationResult.cs @@ -1,4 +1,4 @@ -namespace Unity.AI.Runtime +namespace Unity.AI.Runtime.Execution { public enum AIOperationOutcome { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateProvider.cs similarity index 93% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateProvider.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateProvider.cs index 68a08cf2e9..25356535c0 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateProvider.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Volo.Abp.DependencyInjection; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public class AIPromptTemplateProvider( IAIPromptTemplateStore promptTemplateStore) : IAIPromptTemplateProvider, ITransientDependency diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateRenderer.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateRenderer.cs similarity index 99% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateRenderer.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateRenderer.cs index 67e248c811..e3ea054258 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateRenderer.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateRenderer.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Text.Json; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public static class AIPromptTemplateRenderer { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateSnapshot.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateSnapshot.cs similarity index 81% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateSnapshot.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateSnapshot.cs index 7f78c2879f..09e4c474a4 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateSnapshot.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateSnapshot.cs @@ -1,6 +1,6 @@ using System; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public sealed record AIPromptTemplateSnapshot( string PromptVersion, diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateStore.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateStore.cs similarity index 68% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateStore.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateStore.cs index bd7d0c5037..01c0892825 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIPromptTemplateStore.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIPromptTemplateStore.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Unity.AI.Domain; @@ -7,11 +8,12 @@ using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public class AIPromptTemplateStore( IRepository promptRepository, - IDataFilter multiTenantDataFilter) : IAIPromptTemplateStore, ITransientDependency + IDataFilter multiTenantDataFilter, + ICurrentTenant currentTenant) : IAIPromptTemplateStore, ITransientDependency { public async Task GetRequiredPromptAsync( string promptType, @@ -23,9 +25,13 @@ public async Task GetRequiredPromptAsync( using (multiTenantDataFilter.Disable()) { - var prompt = await promptRepository.FindAsync(p => - p.TenantId == null && p.Name == promptType && p.VersionNumber == versionNumber, + var prompts = await promptRepository.GetListAsync( + p => p.Name == promptType && p.VersionNumber == versionNumber && p.IsActive, cancellationToken: cancellationToken); + var prompt = currentTenant.Id is Guid tenantId + ? prompts.FirstOrDefault(p => p.TenantId == tenantId) + : null; + prompt ??= prompts.FirstOrDefault(p => p.TenantId == null); if (prompt == null || !prompt.IsActive) { throw new InvalidOperationException( diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderPayloadValidator.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderPayloadValidator.cs similarity index 99% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderPayloadValidator.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderPayloadValidator.cs index 23d9ed10da..5dc14e9603 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderPayloadValidator.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderPayloadValidator.cs @@ -3,7 +3,7 @@ using System.Text.Json; using Unity.AI.Models; -namespace Unity.AI.Runtime +namespace Unity.AI.Runtime.Execution { public static class AIProviderPayloadValidator { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderResponseMetadata.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderResponseMetadata.cs similarity index 85% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderResponseMetadata.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderResponseMetadata.cs index 8144c43e4e..3a6c361e1b 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderResponseMetadata.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderResponseMetadata.cs @@ -1,4 +1,4 @@ -namespace Unity.AI.Runtime +namespace Unity.AI.Runtime.Execution { public sealed record AIProviderResponseMetadata( string? Model, diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderResult.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderResult.cs similarity index 93% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderResult.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderResult.cs index f36b165279..712d817a97 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIProviderResult.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIProviderResult.cs @@ -1,4 +1,4 @@ -namespace Unity.AI.Runtime +namespace Unity.AI.Runtime.Execution { public sealed record AIProviderResult( string Content, diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIResponseJson.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIResponseJson.cs similarity index 97% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIResponseJson.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIResponseJson.cs index 7cbe184ec8..11c281d41d 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIResponseJson.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIResponseJson.cs @@ -1,6 +1,6 @@ using System; -namespace Unity.AI.Runtime +namespace Unity.AI.Runtime.Execution { internal static class AIResponseJson { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIResponseValidationResult.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIResponseValidationResult.cs similarity index 93% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIResponseValidationResult.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIResponseValidationResult.cs index ab0a542056..f9b1bbabd1 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/AIResponseValidationResult.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/AIResponseValidationResult.cs @@ -1,4 +1,4 @@ -namespace Unity.AI.Runtime +namespace Unity.AI.Runtime.Execution { public sealed record AIResponseValidationResult( bool IsValid, diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/IAIPromptTemplateProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/IAIPromptTemplateProvider.cs similarity index 87% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/IAIPromptTemplateProvider.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/IAIPromptTemplateProvider.cs index 44c8bded92..64750fa5a9 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/IAIPromptTemplateProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/IAIPromptTemplateProvider.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public interface IAIPromptTemplateProvider { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/IAIPromptTemplateStore.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/IAIPromptTemplateStore.cs similarity index 87% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/IAIPromptTemplateStore.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/IAIPromptTemplateStore.cs index 96d583b756..6a6a183e7a 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/IAIPromptTemplateStore.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/IAIPromptTemplateStore.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public interface IAIPromptTemplateStore { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIChatClientFactory.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIChatClientFactory.cs similarity index 92% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIChatClientFactory.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIChatClientFactory.cs index d97311e3fb..af4702aba5 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIChatClientFactory.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIChatClientFactory.cs @@ -3,7 +3,7 @@ using System.ClientModel; using Volo.Abp.DependencyInjection; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public class OpenAIChatClientFactory : ITransientDependency { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIConfigurationResolver.cs similarity index 87% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIConfigurationResolver.cs index cdb7573430..e88fb4e731 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIConfigurationResolver.cs @@ -12,7 +12,7 @@ using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public class OpenAIConfigurationResolver( IRepository modelRepository, @@ -20,7 +20,8 @@ public class OpenAIConfigurationResolver( IRepository promptRepository, IMemoryCache memoryCache, IConfiguration configuration, - IDataFilter multiTenantDataFilter) : ITransientDependency + IDataFilter multiTenantDataFilter, + ICurrentTenant currentTenant) : ITransientDependency { private static readonly TimeSpan SettingsCacheDuration = TimeSpan.FromMinutes(5); private static readonly JsonSerializerOptions JsonOptions = new() @@ -34,6 +35,7 @@ public class OpenAIConfigurationResolver( private readonly IMemoryCache _memoryCache = memoryCache; private readonly IConfiguration _configuration = configuration; private readonly IDataFilter _multiTenantDataFilter = multiTenantDataFilter; + private readonly ICurrentTenant _currentTenant = currentTenant; public string ResolveProviderName() => Required("Azure:Operations:Defaults:Provider"); @@ -72,7 +74,7 @@ public async Task ResolveOperationSettingsAsync( { throw new InvalidOperationException($"Azure:{providerName}:Endpoint must be a valid absolute URI."); } - var prompt = await LoadPromptAsync(operation.AIPromptId, cancellationToken); + var prompt = await ResolvePromptAsync(operation.Name, cancellationToken); if (!prompt.IsActive) { throw new InvalidOperationException($"AI prompt '{prompt.Name}' v{prompt.VersionNumber} is not active."); @@ -273,7 +275,7 @@ private async Task ResolvePromptVersionAsyncCore(string operationName, C throw new InvalidOperationException($"AI operation '{operationName}' is not configured."); } - var prompt = await LoadPromptAsync(operation.AIPromptId, cancellationToken); + var prompt = await ResolvePromptAsync(operation.Name, cancellationToken); if (!prompt.IsActive) { throw new InvalidOperationException($"AI prompt '{prompt.Name}' v{prompt.VersionNumber} is not active."); @@ -282,9 +284,9 @@ private async Task ResolvePromptVersionAsyncCore(string operationName, C return $"v{prompt.VersionNumber}"; } - private static string BuildOperationSettingsCacheKey(string operationName) + private string BuildOperationSettingsCacheKey(string operationName) { - return $"ai:operation-settings:{operationName.Trim().ToLowerInvariant()}"; + return $"ai:operation-settings:{_currentTenant.Id?.ToString() ?? "host"}:{operationName.Trim().ToLowerInvariant()}"; } private async Task LoadPromptAsync(Guid promptId, CancellationToken cancellationToken) @@ -319,7 +321,6 @@ private async Task LoadPromptAsync(Guid promptId, CancellationToken ca operation.Id, operation.Name, operation.AIModelId, - operation.AIPromptId, operation.CompletionTokens, operation.IsActive); @@ -327,16 +328,40 @@ private async Task LoadPromptAsync(Guid promptId, CancellationToken ca return snapshot; } - private static string BuildOperationSnapshotCacheKey(string operationName) + private string BuildOperationSnapshotCacheKey(string operationName) { - return $"ai:operation-snapshot:{operationName.Trim().ToLowerInvariant()}"; + return $"ai:operation-snapshot:{_currentTenant.Id?.ToString() ?? "host"}:{operationName.Trim().ToLowerInvariant()}"; + } + + private async Task ResolvePromptAsync(string promptFamily, CancellationToken cancellationToken) + { + using (_multiTenantDataFilter.Disable()) + { + var prompts = await _promptRepository.GetListAsync( + prompt => prompt.Name == promptFamily && prompt.IsActive, + cancellationToken: cancellationToken); + prompts ??= []; + + var selected = _currentTenant.Id is Guid tenantId + ? prompts.Where(prompt => prompt.TenantId == tenantId) + .OrderByDescending(prompt => prompt.VersionNumber) + .FirstOrDefault() + : null; + + selected ??= prompts.Where(prompt => prompt.TenantId == null) + .OrderByDescending(prompt => prompt.VersionNumber) + .FirstOrDefault(); + + return selected + ?? throw new InvalidOperationException( + $"AI prompt family '{promptFamily}' has no active prompt for the current tenant."); + } } private sealed record ResolvedOperationSnapshot( Guid Id, string Name, Guid AIModelId, - Guid AIPromptId, int CompletionTokens, bool IsActive); } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIOperationSettings.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIOperationSettings.cs similarity index 88% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIOperationSettings.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIOperationSettings.cs index e98b6ca104..5a396c6301 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIOperationSettings.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIOperationSettings.cs @@ -1,6 +1,6 @@ using System; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public sealed record OpenAIOperationSettings( string ProviderName, diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptFileLogger.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIPromptFileLogger.cs similarity index 99% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptFileLogger.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIPromptFileLogger.cs index a7317c13c2..7ed4a18c2f 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptFileLogger.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIPromptFileLogger.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using Volo.Abp.DependencyInjection; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public class OpenAIPromptFileLogger( IConfiguration configuration, diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptRenderer.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIPromptRenderer.cs similarity index 99% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptRenderer.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIPromptRenderer.cs index 9080271150..a661ccaff4 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIPromptRenderer.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIPromptRenderer.cs @@ -3,7 +3,7 @@ using System.Text.Json; using Volo.Abp.DependencyInjection; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public class OpenAIPromptRenderer : ITransientDependency { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIResponseParser.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIResponseParser.cs similarity index 99% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIResponseParser.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIResponseParser.cs index 1cb90c3455..9c48f4644b 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIResponseParser.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIResponseParser.cs @@ -7,7 +7,7 @@ using Unity.AI.Responses; using Volo.Abp.DependencyInjection; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public class OpenAIResponseParser : ITransientDependency { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIRuntimeService.cs similarity index 99% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIRuntimeService.cs index 9da405ab77..95eebffec6 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIRuntimeService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAIRuntimeService.cs @@ -7,13 +7,13 @@ using System.Threading.Tasks; using Unity.AI.Models; using Unity.AI.Operations; -using Unity.AI.Prompts; +using Unity.AI.Runtime.Prompts; using Unity.AI.Requests; using Unity.AI.Responses; using Volo.Abp; using Volo.Abp.DependencyInjection; -namespace Unity.AI.Runtime +namespace Unity.AI.Runtime.Execution { [ExposeServices(typeof(IAIService), typeof(IFormMappingService), typeof(IFormWorksheetService), typeof(IFormScoresheetService))] public class OpenAIRuntimeService : IAIService, IFormMappingService, IFormWorksheetService, IFormScoresheetService, ITransientDependency diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAITransportService.cs similarity index 99% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAITransportService.cs index 89d503f1b1..2a86c17a0c 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAITransportService.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Execution/OpenAITransportService.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Volo.Abp.DependencyInjection; -namespace Unity.AI.Runtime; +namespace Unity.AI.Runtime.Execution; public class OpenAITransportService( OpenAIChatClientFactory chatClientFactory, diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/AIPromptTypes.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/AIPromptTypes.cs similarity index 91% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/AIPromptTypes.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/AIPromptTypes.cs index 081410e782..5a245745e9 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/AIPromptTypes.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/AIPromptTypes.cs @@ -1,4 +1,4 @@ -namespace Unity.AI.Prompts; +namespace Unity.AI.Runtime.Prompts; public static class AIPromptTypes { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/PromptDataPayloadBuilder.cs similarity index 99% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/PromptDataPayloadBuilder.cs index cb769ece99..785ff1f9b7 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/PromptDataPayloadBuilder.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/PromptDataPayloadBuilder.cs @@ -7,7 +7,7 @@ using Unity.AI.Operations; using Unity.AI.Models; -namespace Unity.AI.Prompts +namespace Unity.AI.Runtime.Prompts { internal static class PromptDataPayloadBuilder { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/application-analysis.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-analysis.system.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/application-analysis.system.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-analysis.system.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/application-analysis.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-analysis.user.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/application-analysis.user.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-analysis.user.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/application-scoring.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-scoring.system.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/application-scoring.system.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-scoring.system.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/application-scoring.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-scoring.user.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/application-scoring.user.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/application-scoring.user.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/attachment-summary.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/attachment-summary.system.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/attachment-summary.system.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/attachment-summary.system.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/attachment-summary.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/attachment-summary.user.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v0/attachment-summary.user.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v0/attachment-summary.user.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.output.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.output.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.output.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.output.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.rubric.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.rubric.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.rubric.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.rubric.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.rules.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.rules.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.rules.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.rules.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.score.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.score.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.score.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.score.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.system.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.system.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.system.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.user.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-analysis.user.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-analysis.user.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-scoring.output.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.output.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-scoring.output.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.output.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-scoring.rules.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.rules.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-scoring.rules.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.rules.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-scoring.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.system.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-scoring.system.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.system.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-scoring.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.user.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/application-scoring.user.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/application-scoring.user.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/attachment-summary.output.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.output.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/attachment-summary.output.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.output.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/attachment-summary.rules.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.rules.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/attachment-summary.rules.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.rules.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/attachment-summary.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.system.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/attachment-summary.system.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.system.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/attachment-summary.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.user.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/attachment-summary.user.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/attachment-summary.user.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/common.rules.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/common.rules.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v1/common.rules.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v1/common.rules.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v2/form-worksheet.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/form-worksheet.user.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v2/form-worksheet.user.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/form-worksheet.user.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v2/onboarding-mapping.system.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/onboarding-mapping.system.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v2/onboarding-mapping.system.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/onboarding-mapping.system.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v2/onboarding-mapping.user.txt b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/onboarding-mapping.user.txt similarity index 100% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Prompts/Versions/v2/onboarding-mapping.user.txt rename to applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/Versions/v2/onboarding-mapping.user.txt diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj index 8e35c29a78..976f38baa3 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Unity.AI.Application.csproj @@ -33,7 +33,7 @@ - + PreserveNewest PreserveNewest diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml index 4eb0df8069..d03e1355fa 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml @@ -51,7 +51,7 @@ diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.js b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.js index 07f1c714c1..e65026b870 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.js @@ -75,7 +75,7 @@ $(function () { const actionButtons = [ { - text: ' New Prompt', + text: ' New Prompt', titleAttr: 'New Prompt', id: 'CreatePromptButton', className: 'btn btn-light rounded-1', diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/EditDataRowModal.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/EditDataRowModal.cshtml index 47e4666671..251cd9fb5f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/EditDataRowModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/EditDataRowModal.cshtml @@ -47,8 +47,8 @@
@if (!isEditable) { - - + + } else if (fieldType == CustomFieldType.Checkbox) { diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionNumberWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionNumberWidget/Default.cshtml index 074cdfe47e..eb74e0d097 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionNumberWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionNumberWidget/Default.cshtml @@ -11,9 +11,10 @@ id="answer-number-@Model.QuestionId" name="Answer-Number-@Model.QuestionId" value="@Model.Answer" - data-original-value="@Model.Answer" + data-original-value="@Model.Answer" data-is-human-confirmed="@Model.IsHumanConfirmed.ToString().ToLower()" - oninput="handleInputChange('@Model.QuestionId','answer-number-'); markAsHumanConfirmed(this)" + data-original-is-human-confirmed="@Model.IsHumanConfirmed.ToString().ToLower()" + oninput="handleInputChange('@Model.QuestionId','answer-number-'); markAsHumanConfirmed(this)" /> @if (!Model.IsHumanConfirmed && Model.Answer.HasValue) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/Default.cshtml index eea5d0da44..7b50e3438e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/Default.cshtml @@ -10,8 +10,10 @@ + @if (!Model.IsHumanConfirmed && !string.IsNullOrEmpty(Model.Answer)) {
diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListViewModel.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListViewModel.cs index 4f4bf8b601..b61da02057 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListViewModel.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListViewModel.cs @@ -2,7 +2,7 @@ namespace Unity.Flex.Web.Views.Shared.Components.QuestionSelectListWidget { - public class QuestionSelectListViewModel + public class QuestionSelectListViewModel : RequiredFieldViewModel { public Guid QuestionId { get; set; } public bool IsDisabled { get; set; } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListWidget.cs index 7df6ae6cc4..b01559b810 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionSelectListWidget/QuestionSelectListWidget.cs @@ -16,9 +16,9 @@ namespace Unity.Flex.Web.Views.Shared.Components.QuestionSelectListWidget AutoInitialize = true)] public class QuestionSelectListWidget : AbpViewComponent { - public async Task InvokeAsync(Guid questionId, bool isDisabled, string? answer, string definition, bool isHumanConfirmed = true, string? aiCitation = null, int? aiConfidence = null) + public async Task InvokeAsync(Guid questionId, bool isDisabled, string? answer, string definition, bool required = false, bool isHumanConfirmed = true, string? aiCitation = null, int? aiConfidence = null) { - return View(await Task.FromResult(new QuestionSelectListViewModel() { QuestionId = questionId, IsDisabled = isDisabled, Answer = answer ?? string.Empty, Definition = definition, IsHumanConfirmed = isHumanConfirmed, AICitation = aiCitation, AIConfidence = aiConfidence })); + return View(await Task.FromResult(new QuestionSelectListViewModel() { QuestionId = questionId, IsDisabled = isDisabled, Answer = answer ?? string.Empty, Definition = definition, Required = required, IsHumanConfirmed = isHumanConfirmed, AICitation = aiCitation, AIConfidence = aiConfidence })); } public class QuestionSelectListWidgetStyleBundleContributor : BundleContributor diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionTextAreaWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionTextAreaWidget/Default.cshtml index 8c2b873634..2c57048344 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionTextAreaWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionTextAreaWidget/Default.cshtml @@ -14,6 +14,7 @@ name="Answer-Textarea-@Model.QuestionId" data-original-value="@Model.Answer" data-is-human-confirmed="@Model.IsHumanConfirmed.ToString().ToLower()" + data-original-is-human-confirmed="@Model.IsHumanConfirmed.ToString().ToLower()" oninput="handleInputChange('@Model.QuestionId','answer-textarea-'); markAsHumanConfirmed(this)">@Model.Answer @if (!Model.IsHumanConfirmed && !string.IsNullOrEmpty(Model.Answer)) { diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionTextWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionTextWidget/Default.cshtml index c28a1cec76..89ebc93d6e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionTextWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionTextWidget/Default.cshtml @@ -11,8 +11,9 @@ id="answer-text-@Model.QuestionId" name="Answer-Text-@Model.QuestionId" value="@Model.Answer" - data-original-value="@Model.Answer" + data-original-value="@Model.Answer" data-is-human-confirmed="@Model.IsHumanConfirmed.ToString().ToLower()" + data-original-is-human-confirmed="@Model.IsHumanConfirmed.ToString().ToLower()" oninput="handleInputChange('@Model.QuestionId','answer-text-'); markAsHumanConfirmed(this)" /> @if (!Model.IsHumanConfirmed && !string.IsNullOrEmpty(Model.Answer)) { diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/Default.cshtml index 9b267286bd..ae5fe2eaa1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/Default.cshtml @@ -7,16 +7,19 @@ + @if (!Model.IsHumanConfirmed && !string.IsNullOrEmpty(Model.Answer)) {
diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/QuestionYesNoViewModel.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/QuestionYesNoViewModel.cs index 668e50cf4a..14c67adcd7 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/QuestionYesNoViewModel.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/QuestionYesNoViewModel.cs @@ -2,7 +2,7 @@ namespace Unity.Flex.Web.Views.Shared.Components.QuestionYesNoWidget { - public class QuestionYesNoViewModel + public class QuestionYesNoViewModel : RequiredFieldViewModel { public Guid QuestionId { get; set; } public bool IsDisabled { get; set; } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/QuestionYesNoWidget.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/QuestionYesNoWidget.cs index 0d87c61ead..591a06fc3e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/QuestionYesNoWidget.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/QuestionYesNoWidget/QuestionYesNoWidget.cs @@ -16,9 +16,9 @@ namespace Unity.Flex.Web.Views.Shared.Components.QuestionYesNoWidget AutoInitialize = true)] public class QuestionYesNoWidget : AbpViewComponent { - public async Task InvokeAsync(Guid questionId, bool isDisabled, string? answer, string? yesValue, string? noValue, bool isHumanConfirmed = true, string? aiCitation = null, int? aiConfidence = null) + public async Task InvokeAsync(Guid questionId, bool isDisabled, string? answer, string? yesValue, string? noValue, bool required = false, bool isHumanConfirmed = true, string? aiCitation = null, int? aiConfidence = null) { - return View(await Task.FromResult(new QuestionYesNoViewModel() { QuestionId = questionId, IsDisabled = isDisabled, Answer = answer ?? string.Empty, YesValue = yesValue, NoValue = noValue, IsHumanConfirmed = isHumanConfirmed, AICitation = aiCitation, AIConfidence = aiConfidence })); + return View(await Task.FromResult(new QuestionYesNoViewModel() { QuestionId = questionId, IsDisabled = isDisabled, Answer = answer ?? string.Empty, YesValue = yesValue, NoValue = noValue, Required = required, IsHumanConfirmed = isHumanConfirmed, AICitation = aiCitation, AIConfidence = aiConfidence })); } public class QuestionYesNoWidgetStyleBundleContributor : BundleContributor diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.css b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.css index 3b221db113..4df0563597 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.css +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.css @@ -22,6 +22,53 @@ color: #ffffff; } +#previewScoresheetActions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + position: sticky; + top: 0; + padding: 8px 24px 8px 0; + z-index: 999; + background-color: #ffffff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06); +} + +#previewScoresheetActions .floating-save-btn { + position: static; +} + +#previewSaveAllBtn:disabled { + --bs-btn-disabled-bg: #255A90; + --bs-btn-disabled-color: #ffffff; +} + +#previewDiscardAllBtn:disabled { + --bs-btn-disabled-color: #6c757d; +} + +#preview .accordion-button.section-unsaved, +#scoresheet-preview .accordion-button.section-unsaved { + color: #FF0909; +} + +#preview .preview-btn.section-unsaved:not(.collapsed), +#scoresheet-preview .preview-btn.section-unsaved:not(.collapsed) { + color: #FF0909; + background-color: #FEEAEA; +} + +#preview .accordion-button.question-unsaved, +#scoresheet-preview .accordion-button.question-unsaved { + color: #FF0909; +} + +#preview .accordion-button.question-unsaved:not(.collapsed), +#scoresheet-preview .accordion-button.question-unsaved:not(.collapsed) { + color: #FF0909; + background-color: #FEEAEA; +} + #preview .preview-btn.collapsed::after { -webkit-filter: grayscale(1) invert(0); filter: grayscale(1) invert(0); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js index 24f9a42dc4..c670b00b18 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js @@ -165,6 +165,11 @@ $(function () { function updatePreviewAccordion(sortedItems) { const previewDiv = document.getElementById('scoresheet-preview') || document.getElementById('preview'); + // The whole preview is rebuilt from scratch here (drag/drop reorder, + // switching scoresheets, etc.) - any tracked dirty/invalid state from + // the previous render no longer maps to anything in the new DOM. + resetPreviewSectionState(); + if (sortedItems.length === 0) { previewDiv.innerHTML = '

No sections to display.

'; return; @@ -188,16 +193,6 @@ $(function () { if (item.classList.contains('section-item')) { if (currentSectionItem) { accordionHTML += `
-
- - -
`; @@ -222,7 +217,7 @@ $(function () { accordionHTML += `

-

@@ -238,22 +233,16 @@ $(function () { if (currentSectionItem) { accordionHTML += `
-
- - -
`; } previewDiv.innerHTML = ` +
+ + +
@@ -277,7 +266,7 @@ $(function () { } function buildTextAreaFieldPreview(item) { - let req = item.dataset.required ? "required" : null; + let req = item.dataset.required === 'True' ? "required" : null; return `

${sanitizeHtml(item.dataset.questiondesc)}

@@ -296,6 +285,7 @@ $(function () { const truncatedValue = option.value.length > 100 ? option.value.substring(0, 100) + " ..." : option.value; return ``; }).join(''); + let req = item.dataset.required === 'True' ? "required" : null; return `

${sanitizeHtml(item.dataset.questiondesc)}

@@ -304,15 +294,17 @@ $(function () { +
`; } function buildNumberFieldPreview(item) { - let req = item.dataset.required ? "required" : null; + let req = item.dataset.required === 'True' ? "required" : null; return `

${sanitizeHtml(item.dataset.questiondesc)}

@@ -325,6 +317,8 @@ $(function () { } function buildYesNoFieldPreview(item) { + let req = item.dataset.required === 'True' ? "required" : null; + return `

${sanitizeHtml(item.dataset.questiondesc)}

@@ -332,6 +326,7 @@ $(function () { +
`; } function buildTextFieldPreview(item) { - let req = item.dataset.required ? "required" : null; + let req = item.dataset.required === 'True' ? "required" : null; return `

${sanitizeHtml(item.dataset.questiondesc)}

@@ -497,24 +493,124 @@ function savePreviewChanges(questionId, inputFieldPrefix, saveButtonPrefix, disc updateSubtotal(); } -function savePreviewSectionChanges(formId, sectionId) { - const secSaveButton = document.getElementById('scoresheet-section-save-' + sectionId); - const secDiscardButton = document.getElementById('scoresheet-section-discard-' + sectionId); +// sectionId (hashCode-derived) -> { isDirty: bool, isValid: bool } +// Kept separate from AssessmentScoresWidget's scoresheetSectionState: this +// preview has no assessment, no backend save, and its own hashCode-based +// section id scheme, so it tracks and acts on its own bulk button pair. +let previewSectionState = {}; + +function resetPreviewSectionState() { + previewSectionState = {}; + refreshPreviewBulkActionButtons(); +} + +function getDirtyPreviewSectionIds() { + return Object.keys(previewSectionState).filter( + (id) => previewSectionState[id].isDirty + ); +} + +function updatePreviewSectionHeaderStyle(sectionId, isDirty) { + const headerButton = document.querySelector( + '#panel-' + sectionId + ' .accordion-button' + ); + if (headerButton) { + headerButton.classList.toggle('section-unsaved', isDirty); + } +} - const assessmentAnswersArr = []; - const inputFieldArr = []; - const origAnswersArr = []; - const formData = $(`#${formId}`).serializeArray(); +function refreshPreviewBulkActionButtons() { + const states = Object.values(previewSectionState); + const anyDirty = states.some((s) => s.isDirty); + const anyInvalidDirty = states.some((s) => s.isDirty && !s.isValid); - //Handle form object data - $.each(formData, function (_, inputData) { - buildFormData(assessmentAnswersArr, inputData, inputFieldArr, origAnswersArr); + const saveAllBtn = document.getElementById('previewSaveAllBtn'); + const discardAllBtn = document.getElementById('previewDiscardAllBtn'); + if (discardAllBtn) discardAllBtn.disabled = !anyDirty; + if (saveAllBtn) saveAllBtn.disabled = !anyDirty || anyInvalidDirty; +} + +// Registered as an extension hook that AssessmentScoresWidget/Default.js's +// shared handleInputChange() calls after validating a section - this script +// bundles alongside it on the Scoresheet configuration page (see +// ScoresheetViewComponent's script bundle contributor). +globalThis.onScoresheetSectionValidated = function (sectionId, isDirty, isInvalid) { + previewSectionState[sectionId] = { isDirty, isValid: !isInvalid }; + updatePreviewSectionHeaderStyle(sectionId, isDirty); + refreshPreviewBulkActionButtons(); +}; + +// Local-only "save": there is no real assessment to persist to here, this +// just accepts the current values as the new baseline, same as clicking +// Save All would visually communicate on the real page. +function savePreviewAllSections() { + const dirtySectionIds = getDirtyPreviewSectionIds(); + if (dirtySectionIds.length === 0) return; + + dirtySectionIds.forEach((sectionId) => { + const answersArr = []; + const inputFieldArr = []; + const origAnswersArr = []; + $.each( + $(`#section-form-${sectionId}`).serializeArray(), + function (_, inputData) { + buildFormData(answersArr, inputData, inputFieldArr, origAnswersArr); + } + ); + + inputFieldArr.forEach((fieldId) => { + const el = document.getElementById(fieldId); + if (el) { + el.dataset.originalValue = el.value; + } + const questionId = fieldId.split('-').slice(2).join('-'); + updateQuestionHeaderStyle(questionId, false); + }); + + previewSectionState[sectionId] = { isDirty: false, isValid: true }; + updatePreviewSectionHeaderStyle(sectionId, false); }); - secSaveButton.disabled = true; - secDiscardButton.disabled = true; + updateSubtotal(); + refreshPreviewBulkActionButtons(); +} + +function discardAllPreviewSections() { + const dirtySectionIds = getDirtyPreviewSectionIds(); + if (dirtySectionIds.length === 0) return; + + dirtySectionIds.forEach((sectionId) => { + const answersArr = []; + const inputFieldArr = []; + const origAnswersArr = []; + $.each( + $(`#section-form-${sectionId}`).serializeArray(), + function (_, inputData) { + buildFormData(answersArr, inputData, inputFieldArr, origAnswersArr); + } + ); + + inputFieldArr.forEach((fieldId) => { + const questionId = fieldId.split('-').slice(2).join('-'); + const el = document.getElementById(fieldId); + el.value = el.dataset.originalValue; + + const errorMessage = document.getElementById( + 'error-message-' + questionId + ); + if (errorMessage) { + errorMessage.textContent = ''; + } + + updateQuestionHeaderStyle(questionId, false); + }); + + previewSectionState[sectionId] = { isDirty: false, isValid: true }; + updatePreviewSectionHeaderStyle(sectionId, false); + }); updateSubtotal(); + refreshPreviewBulkActionButtons(); } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetInstanceWidget/Default.css b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetInstanceWidget/Default.css index 15c648cc60..475de28ae1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetInstanceWidget/Default.css +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetInstanceWidget/Default.css @@ -1,6 +1,6 @@ .control-render-error { display: block; - background-color: red; + background-color: #cc0000; text-align: center; padding: 5px; color: #fff; diff --git a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Navigation/UnityIdentityWebMainMenuContributor.cs b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Navigation/UnityIdentityWebMainMenuContributor.cs index 9d7f2b6896..2579dcbeb8 100644 --- a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Navigation/UnityIdentityWebMainMenuContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Navigation/UnityIdentityWebMainMenuContributor.cs @@ -17,7 +17,7 @@ public virtual Task ConfigureMenuAsync(MenuConfigurationContext context) var l = context.GetLocalizer(); - var identityMenuItem = new ApplicationMenuItem(UnityIdentityMenuNames.GroupName, l["Menu:IdentityManagement"], icon: "fa fa-id-card-o"); + var identityMenuItem = new ApplicationMenuItem(UnityIdentityMenuNames.GroupName, l["Menu:IdentityManagement"], icon: "fa-regular fa-id-card"); identityMenuItem.AddItem(new ApplicationMenuItem(UnityIdentityMenuNames.Roles, l["Roles"], url: "~/Identity/Roles").RequirePermissions(IdentityPermissions.Roles.Default)); identityMenuItem.AddItem(new ApplicationMenuItem(UnityIdentityMenuNames.Users, l["Users"], url: "~/Identity/Users").RequirePermissions(IdentityPermissions.Users.Default)); diff --git a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.js b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.js index a5189339e8..094f1f124a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.js +++ b/applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.js @@ -194,7 +194,7 @@ $(function () { if (!row.isActive) { return ' ' + + '" class="fa-solid fa-ban text-danger"> ' + '' + row.userName + ''; } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs index 0abcfd1e37..f350797571 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs @@ -4,12 +4,13 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; -using System.IO; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Unity.AspNetCore.Mvc.UI.Theme.UX2.Renderers; using Unity.GrantManager.Notifications; using Unity.Notifications.Emails; using Unity.Notifications.Permissions; @@ -32,7 +33,8 @@ public class EmailNotificationService( ISettingManager settingManager, IFeatureChecker featureChecker, IConfiguration configuration, - IWebHostEnvironment webHostEnvironment) : ApplicationService, IEmailNotificationService + IWebHostEnvironment webHostEnvironment, + IMarkdownRenderer markdownRenderer) : ApplicationService, IEmailNotificationService { public async Task InitializeDraftAsync(Guid applicationId) @@ -251,7 +253,7 @@ private async Task UpdateTenantSettings(string settingKey, string valueString) /// Renders the comment notification email template with the provided parameters. /// /// Display name of the user who mentioned - /// The comment body text (may contain HTML) + /// The comment body text /// The URL link to view the comment /// Rendered HTML email body private async Task RenderCommentNotificationTemplateAsync(string currentUserText, string commentBody, string commentLink) @@ -259,11 +261,16 @@ private async Task RenderCommentNotificationTemplateAsync(string current // Load template from embedded resources or file system string templateContent = await LoadEmailTemplateAsync("CommentNotification"); + var encodedCurrentUserText = WebUtility.HtmlEncode(currentUserText); + var encodedCommentBody = markdownRenderer.Render(commentBody); + var encodedCommentLink = WebUtility.HtmlEncode(commentLink); + // Replace placeholders with actual values var renderedTemplate = templateContent - .Replace("@Model.CurrentUserText", currentUserText) - .Replace("@Html.Raw(Model.CommentBody)", commentBody) - .Replace("@Model.CommentLink", commentLink); + .Replace("@Model.CurrentUserText", encodedCurrentUserText) + .Replace("@Html.Raw(Model.CommentBody)", encodedCommentBody) + .Replace("@Model.CommentLink", encodedCommentLink) + .Replace("@model dynamic", string.Empty); return renderedTemplate; } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Unity.Notifications.Application.csproj b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Unity.Notifications.Application.csproj index 7a8eacb9a4..5dd709e5a3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Unity.Notifications.Application.csproj +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Unity.Notifications.Application.csproj @@ -24,6 +24,7 @@ + diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js index 538c9c590a..8092c7b82c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js @@ -37,7 +37,7 @@ const emailGroupsManager = { const buttonClass = row.isNew ? 'remove-selected-user' : 'remove-user-btn'; const dataAttr = row.isNew ? `data-user-id="${row.userId}"` : `data-group-user-id="${row.id}"`; return ``; } } @@ -63,21 +63,33 @@ const emailGroupsManager = { }; }, + // Escape untrusted values for safe insertion into HTML text and attribute contexts + escapeHtml: function(value) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll("\"", '"') + .replaceAll('\'', '''); + }, + // Generate dropdown item HTML for users generateUserDropdownItem: function(user) { const firstName = user.name ? user.name.split(' ')[0] : ''; const lastName = user.surname || ''; + const displayName = this.escapeHtml(user.userName || user.name || 'Unknown'); + const email = this.escapeHtml(user.email || ''); return `
  • + data-user-id="${this.escapeHtml(user.id)}" + data-user-name="${displayName}" + data-user-email="${email}" + data-first-name="${this.escapeHtml(firstName)}" + data-last-name="${this.escapeHtml(lastName)}">
    - ${user.userName || user.name || 'Unknown'} - ${user.email || ''} + ${displayName} + ${email}
  • @@ -372,7 +384,7 @@ const emailGroupsManager = {

    @@ -559,6 +571,9 @@ const emailGroupsManager = { let pendingUserAdditions = []; let pendingUserRemovals = []; + const escapedGroupName = emailGroupsManager.utils.escapeHtml(group.name); + const escapedGroupDescription = emailGroupsManager.utils.escapeHtml(group.description || ''); + const modalHtml = `
    - + Tip: Use a descriptive name that clearly identifies the data this view will contain.
    @@ -261,7 +261,7 @@