From d1f6fb71e08f6e44071cb08e591ca720e4a3ee77 Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Tue, 4 Aug 2026 14:32:08 -0700 Subject: [PATCH 1/5] AB#33943 cater for views saved in previous version --- .../wwwroot/themes/ux2/table-utils.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js index 981f3ec37..6081fe1cd 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js @@ -353,9 +353,24 @@ function initializeDataTable(options) { } }, stateLoadParams: function (settings, data) { - // Discard stale state when column count has changed so defaultVisibleColumns applies cleanly + // Remap saved column state onto the live column list by name, rather than + // discarding the whole saved state, so that state (including named "Saved + // Views") saved before columns were added/removed/reordered can still be + // loaded. Columns that didn't exist when the state was saved fall back to + // the table's configured default visibility (defaultVisibleColumns). if (data?.columns && data.columns.length !== settings.aoColumns.length) { - return false; + const savedColumnsByName = new Map( + data.columns.map(function (col) { return [col.name, col]; }) + ); + data.columns = settings.aoColumns.map(function (col) { + const saved = savedColumnsByName.get(col.sName); + if (saved) return saved; + return { + name: col.sName, + visible: col.bVisible, + search: { search: '', smart: true, regex: false, caseInsensitive: true, return: false } + }; + }); } if (data?.externalSearch) { From d979e9ce99c56e45b26f46b28dfa0d2994df270c Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Tue, 4 Aug 2026 15:23:48 -0700 Subject: [PATCH 2/5] AB#33943 address codeQL feedback --- .../wwwroot/themes/ux2/table-utils.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js index 6081fe1cd..c71785e9f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js @@ -354,19 +354,21 @@ function initializeDataTable(options) { }, stateLoadParams: function (settings, data) { // Remap saved column state onto the live column list by name, rather than - // discarding the whole saved state, so that state (including named "Saved - // Views") saved before columns were added/removed/reordered can still be - // loaded. Columns that didn't exist when the state was saved fall back to - // the table's configured default visibility (defaultVisibleColumns). - if (data?.columns && data.columns.length !== settings.aoColumns.length) { + // applying it positionally, so that state (including named "Saved Views") + // saved before columns were added/removed/reordered can still be loaded + // correctly. Always remapped (not just on a length mismatch) since a + // same-count reorder or rename would otherwise still be misapplied by + // position. Columns with no matching saved name fall back to the table's + // configured default visibility (defaultVisibleColumns). + if (data?.columns) { const savedColumnsByName = new Map( data.columns.map(function (col) { return [col.name, col]; }) ); data.columns = settings.aoColumns.map(function (col) { - const saved = savedColumnsByName.get(col.sName); + const saved = savedColumnsByName.get(col.name); if (saved) return saved; return { - name: col.sName, + name: col.name, visible: col.bVisible, search: { search: '', smart: true, regex: false, caseInsensitive: true, return: false } }; From 2a39a50711a9f4b39b17076e8da7927b5ef40af9 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 4 Aug 2026 15:35:45 -0700 Subject: [PATCH 3/5] AB#33799 decouple reusable AI module from Grant Manager Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../IAttachmentSummaryPersistence.cs | 14 ------ .../Attachments/AttachmentContentStream.cs | 20 ++++++++ .../Attachments/AttachmentSummaryResultDto.cs | 2 +- .../Attachments/IAttachmentContentProvider.cs | 9 ++++ .../AIGenerationOperationKeyHelper.cs | 25 ++++++++++ .../Generation/IAIGenerationStatusReader.cs | 9 ++++ .../ApplicationAnalysisResultDto.cs | 2 +- .../ApplicationContentResultDto.cs | 2 +- .../ApplicationScoringResultDto.cs | 2 +- .../AttachmentSummaryPersistence.cs | 47 ------------------- .../AI/Operations/AttachmentSummaryService.cs | 16 +++---- .../AIApplicationModule.cs | 4 +- .../DataSeed/AIOperationDataSeeder.cs | 1 - .../Generation/AIGenerationAppService.cs | 19 ++++---- .../Unity.AI.Application.csproj | 2 - .../Intakes/ChefsFileAttachmentStream.cs | 28 ----------- .../IChefsFileAttachmentStreamProvider.cs | 9 ---- .../AIGenerationPrerequisiteValidator.cs | 8 ++-- .../Automation/AIGenerationStatusReader.cs | 33 +++++++++++++ .../ChefsFileAttachmentStreamProvider.cs | 11 +++-- .../AttachmentSummaryServiceTests.cs | 42 ++++++++--------- .../Automation/AIGenerationAppServiceTests.cs | 10 ++-- 22 files changed, 154 insertions(+), 161 deletions(-) delete mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAttachmentSummaryPersistence.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/AttachmentContentStream.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentContentProvider.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationOperationKeyHelper.cs create mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/IAIGenerationStatusReader.cs delete mode 100644 applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryPersistence.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ChefsFileAttachmentStream.cs delete mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/IChefsFileAttachmentStreamProvider.cs rename applications/Unity.GrantManager/{modules/Unity.AI/src/Unity.AI.Application/AI/Operations => src/Unity.GrantManager.Application/GrantApplications/Automation}/AIGenerationPrerequisiteValidator.cs (97%) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AIGenerationStatusReader.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAttachmentSummaryPersistence.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAttachmentSummaryPersistence.cs deleted file mode 100644 index e2a803d1d..000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/AI/Operations/IAttachmentSummaryPersistence.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Unity.AI.Operations; - -public interface IAttachmentSummaryPersistence -{ - Task LoadAsync(Guid attachmentId); - - Task SaveSummaryAsync(Guid attachmentId, string summary); - - Task> LoadApplicationAttachmentIdsAsync(Guid applicationId); -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/AttachmentContentStream.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/AttachmentContentStream.cs new file mode 100644 index 000000000..e5babf8a1 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/AttachmentContentStream.cs @@ -0,0 +1,20 @@ +using System; +using System.IO; +using System.Threading.Tasks; + +namespace Unity.AI.Attachments; + +public sealed class AttachmentContentStream(Stream content, string contentType) : IDisposable, IAsyncDisposable +{ + public Stream Content { get; } = content ?? throw new ArgumentNullException(nameof(content)); + + public string ContentType { get; } = + string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType; + + public static AttachmentContentStream Empty { get; } = + new(Stream.Null, "application/octet-stream"); + + public void Dispose() => Content.Dispose(); + + public ValueTask DisposeAsync() => Content.DisposeAsync(); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/AttachmentSummaryResultDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/AttachmentSummaryResultDto.cs index b7100305b..e97287e90 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/AttachmentSummaryResultDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/AttachmentSummaryResultDto.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.Attachments; +namespace Unity.AI.Attachments; public class AttachmentSummaryResultDto { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentContentProvider.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentContentProvider.cs new file mode 100644 index 000000000..7e7d2ef1f --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Attachments/IAttachmentContentProvider.cs @@ -0,0 +1,9 @@ +using System; +using System.Threading.Tasks; + +namespace Unity.AI.Attachments; + +public interface IAttachmentContentProvider +{ + Task OpenAttachmentAsync(Guid submissionId, Guid fileId, string name); +} 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 new file mode 100644 index 000000000..5b2a309e8 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/AIGenerationOperationKeyHelper.cs @@ -0,0 +1,25 @@ +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 static string? ResolveOperationName(string operationType) + { + return operationType switch + { + ApplicationAnalysisOperationType => "ApplicationAnalysis", + AttachmentSummaryOperationType => "AttachmentSummary", + ApplicationScoringOperationType => "ApplicationScoring", + FormMappingOperationType => "FormMapping", + FormWorksheetOperationType => "FormWorksheet", + FormScoresheetOperationType => "FormScoresheet", + _ => null + }; + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/IAIGenerationStatusReader.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/IAIGenerationStatusReader.cs new file mode 100644 index 000000000..87234aecf --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Generation/IAIGenerationStatusReader.cs @@ -0,0 +1,9 @@ +using System; +using System.Threading.Tasks; + +namespace Unity.AI.Generation; + +public interface IAIGenerationStatusReader +{ + Task GetLatestAsync(Guid applicationId, string operationType, Guid? tenantId = null); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationAnalysisResultDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationAnalysisResultDto.cs index 19c829756..e4fbd648b 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationAnalysisResultDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationAnalysisResultDto.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.GrantApplications; +namespace Unity.AI.GrantApplications; public class ApplicationAnalysisResultDto { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationContentResultDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationContentResultDto.cs index 034912719..a8c0bafca 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationContentResultDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationContentResultDto.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.GrantApplications; +namespace Unity.AI.GrantApplications; public class ApplicationContentResultDto { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationScoringResultDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationScoringResultDto.cs index b4e4e8621..d548cbb29 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationScoringResultDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/GrantApplications/ApplicationScoringResultDto.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.GrantApplications; +namespace Unity.AI.GrantApplications; public class ApplicationScoringResultDto { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryPersistence.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryPersistence.cs deleted file mode 100644 index baf2fe799..000000000 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AttachmentSummaryPersistence.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Unity.AI.Operations; -using Unity.GrantManager.Applications; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Uow; - -namespace Unity.AI.Operations; - -public class AttachmentSummaryPersistence( - IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, - IUnitOfWorkManager unitOfWorkManager) : IAttachmentSummaryPersistence, ITransientDependency -{ - public async Task LoadAsync(Guid attachmentId) - { - using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); - var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); - var source = new AttachmentSummarySource( - attachment.Id, - attachment.FileName, - attachment.ChefsSubmissionId, - attachment.ChefsFileId); - await uow.CompleteAsync(); - return source; - } - - public async Task SaveSummaryAsync(Guid attachmentId, string summary) - { - using var uow = unitOfWorkManager.Begin(requiresNew: true); - var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); - attachment.AISummary = summary; - await applicationChefsFileAttachmentRepository.UpdateAsync(attachment); - await uow.CompleteAsync(); - } - - public async Task> LoadApplicationAttachmentIdsAsync(Guid applicationId) - { - using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); - var ids = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId)) - .Select(a => a.Id) - .ToList(); - await uow.CompleteAsync(); - return ids; - } -} 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/AI/Operations/AttachmentSummaryService.cs index 89dacc0e0..db433f7ab 100644 --- 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/AI/Operations/AttachmentSummaryService.cs @@ -7,9 +7,9 @@ using System.Threading; using System.Threading.Tasks; using Unity.AI.Extraction; +using Unity.AI.Attachments; using Unity.AI.Localization; using Unity.AI.Requests; -using Unity.GrantManager.Intakes; using Volo.Abp; using Volo.Abp.DependencyInjection; using Volo.Abp.Uow; @@ -18,7 +18,7 @@ namespace Unity.AI.Operations; public class AttachmentSummaryService( IAttachmentSummaryDataProvider attachmentSummaryDataProvider, - IChefsFileAttachmentStreamProvider chefsFileAttachmentStreamProvider, + IAttachmentContentProvider attachmentContentProvider, ITextExtractionService textExtractionService, IAIService aiService, IAIGenerationPrerequisiteValidator aiGenerationPrerequisiteValidator, @@ -149,7 +149,7 @@ private async Task WithUnitOfWorkAsync(Func operation) await uow.CompleteAsync(); } - private async Task OpenAttachmentStreamAsync( + private async Task OpenAttachmentStreamAsync( AttachmentSummarySource attachment, string fileName, CancellationToken cancellationToken) @@ -160,14 +160,14 @@ private async Task OpenAttachmentStreamAsync( logger.LogWarning( "Attachment {AttachmentId} has invalid CHEFS IDs. Falling back to metadata-only summary generation.", attachment.Id); - return ChefsFileAttachmentStream.Empty; + return AttachmentContentStream.Empty; } try { cancellationToken.ThrowIfCancellationRequested(); - var stream = await chefsFileAttachmentStreamProvider.OpenAsync(submissionId, fileId, fileName); - return stream ?? ChefsFileAttachmentStream.Empty; + var stream = await attachmentContentProvider.OpenAttachmentAsync(submissionId, fileId, fileName); + return stream ?? AttachmentContentStream.Empty; } catch (OperationCanceledException) { @@ -179,7 +179,7 @@ private async Task OpenAttachmentStreamAsync( ex, "Failed retrieving CHEFS content for attachment {AttachmentId}. Falling back to metadata-only summary generation.", attachment.Id); - return ChefsFileAttachmentStream.Empty; + return AttachmentContentStream.Empty; } } @@ -197,7 +197,7 @@ private static bool IsSupportedOfficeOrPdf(string fileName) private void LogEmptyExtraction( Guid attachmentId, string fileName, - ChefsFileAttachmentStream attachmentStream) + AttachmentContentStream attachmentStream) { logger.LogWarning( "No text extracted for supported attachment {AttachmentId} ({FileName}). Skipping AI summary generation. ContentType: {ContentType}; StreamCanSeek: {StreamCanSeek}; StreamLength: {StreamLength}.", diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs index 1c1ed15bd..acb835451 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Unity.Flex; -using Unity.GrantManager; using Volo.Abp.Application; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Mapperly; @@ -16,8 +15,7 @@ namespace Unity.AI; typeof(AbpDddApplicationModule), typeof(AbpMapperlyModule), typeof(AbpTenantManagementDomainModule), - typeof(FlexApplicationModule), - typeof(GrantManagerDomainModule) + typeof(FlexApplicationModule) )] public class AIApplicationModule : AbpModule { 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 1f0d8f438..748a18732 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 @@ -6,7 +6,6 @@ using Unity.AI.Operations; using Unity.AI.Domain; using Unity.AI.Prompts; -using Unity.GrantManager.GrantApplications; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; 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 6c9fd867d..c297fef32 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 @@ -7,9 +7,6 @@ using Unity.AI.Operations; using Unity.AI.Permissions; using Unity.AI.Settings; -using Unity.GrantManager.Attachments; -using Unity.GrantManager.GrantApplications; -using Unity.GrantManager.GrantApplications.Automation; using Volo.Abp.MultiTenancy; using Volo.Abp; using Volo.Abp.Features; @@ -19,7 +16,7 @@ namespace Unity.AI.Generation; [Route("api/app/ai/generation")] public class AIGenerationAppService( IApplicationGenerationQueue aiGenerationQueue, - IAIGenerationStatusAppService aiGenerationStatusAppService, + IAIGenerationStatusReader aiGenerationStatusReader, AIFeatureGuard featureGuard, ICurrentTenant currentTenant) : AIAppService, IAIGenerationAppService @@ -105,7 +102,7 @@ public virtual async Task GetStatusAsync(Guid application { await EnsureStatusAccessAsync(operationType); - var request = await aiGenerationStatusAppService.GetLatestAsync(applicationId, operationType, currentTenant.Id); + var request = await aiGenerationStatusReader.GetLatestAsync(applicationId, operationType, currentTenant.Id); if (request == null) { return new AIGenerationStatusDto(); @@ -141,12 +138,12 @@ private async Task EnsureStatusAccessAsync(string operationType) { var permission = operationType switch { - AIGenerationRequestKeyHelper.ApplicationAnalysisOperationType => AIPermissions.Analysis.ViewApplicationAnalysis, - AIGenerationRequestKeyHelper.AttachmentSummaryOperationType => AIPermissions.Analysis.ViewAttachmentSummary, - AIGenerationRequestKeyHelper.ApplicationScoringOperationType => AIPermissions.Analysis.ViewScoringResult, - AIGenerationRequestKeyHelper.FormMappingOperationType => AIPermissions.Analysis.ViewFormMapping, - AIGenerationRequestKeyHelper.FormWorksheetOperationType => AIPermissions.Analysis.ViewFormWorksheet, - AIGenerationRequestKeyHelper.FormScoresheetOperationType => AIPermissions.Analysis.ViewFormScoresheet, + 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}") }; 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 91c741e44..8e35c29a7 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 @@ -22,8 +22,6 @@ - - diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ChefsFileAttachmentStream.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ChefsFileAttachmentStream.cs deleted file mode 100644 index ed9f19f4b..000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/ChefsFileAttachmentStream.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; - -namespace Unity.GrantManager.Intakes; - -/// -/// Stream of a CHEFS file attachment plus its content type. -/// The Content stream owns its underlying temp file; dispose to release. -/// -public sealed class ChefsFileAttachmentStream : IDisposable, IAsyncDisposable -{ - public Stream Content { get; } - public string ContentType { get; } - - public ChefsFileAttachmentStream(Stream content, string contentType) - { - Content = content ?? throw new ArgumentNullException(nameof(content)); - ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType; - } - - public static ChefsFileAttachmentStream Empty { get; } = - new(Stream.Null, "application/octet-stream"); - - public void Dispose() => Content.Dispose(); - - public ValueTask DisposeAsync() => Content.DisposeAsync(); -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/IChefsFileAttachmentStreamProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/IChefsFileAttachmentStreamProvider.cs deleted file mode 100644 index 36bc4b778..000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Intakes/IChefsFileAttachmentStreamProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Unity.GrantManager.Intakes; - -public interface IChefsFileAttachmentStreamProvider -{ - Task OpenAsync(Guid formSubmissionId, Guid chefsFileAttachmentId, string name); -} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIGenerationPrerequisiteValidator.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AIGenerationPrerequisiteValidator.cs similarity index 97% rename from applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIGenerationPrerequisiteValidator.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AIGenerationPrerequisiteValidator.cs index 26923afa8..44a42d67d 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Operations/AIGenerationPrerequisiteValidator.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AIGenerationPrerequisiteValidator.cs @@ -1,16 +1,16 @@ -using Microsoft.Extensions.Localization; using System; using System.Linq; using System.Threading.Tasks; -using Unity.Flex.Domain.Scoresheets; +using Microsoft.Extensions.Localization; using Unity.AI.Localization; +using Unity.AI.Operations; +using Unity.Flex.Domain.Scoresheets; using Unity.GrantManager.Applications; -using Unity.Modules.Shared.Correlation; using Volo.Abp; using Volo.Abp.DependencyInjection; using Volo.Abp.Linq; -namespace Unity.AI.Operations; +namespace Unity.GrantManager.GrantApplications.Automation; public class AIGenerationPrerequisiteValidator( IApplicationRepository applicationRepository, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AIGenerationStatusReader.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AIGenerationStatusReader.cs new file mode 100644 index 000000000..6e827d472 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/Automation/AIGenerationStatusReader.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Unity.AI.Generation; +using Unity.GrantManager.GrantApplications; +using Volo.Abp.DependencyInjection; + +namespace Unity.GrantManager.GrantApplications.Automation; + +public class AIGenerationStatusReader( + IAIGenerationStatusAppService statusAppService) : IAIGenerationStatusReader, ITransientDependency +{ + public async Task GetLatestAsync( + Guid applicationId, + string operationType, + Guid? tenantId = null) + { + var status = await statusAppService.GetLatestAsync(applicationId, operationType, tenantId); + return status == null + ? null + : new Unity.AI.Generation.AIGenerationRequestDto + { + Id = status.Id, + ApplicationId = status.ApplicationId, + OperationId = status.OperationId, + OperationType = status.OperationType, + Status = status.Status.ToString(), + StartedAt = status.StartedAt, + CompletedAt = status.CompletedAt, + FailureReason = status.FailureReason, + IsActive = status.IsActive + }; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsFileAttachmentStreamProvider.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsFileAttachmentStreamProvider.cs index 07e9587f7..7806cbaf1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsFileAttachmentStreamProvider.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/Intakes/ChefsFileAttachmentStreamProvider.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Threading.Tasks; +using Unity.AI.Attachments; using Volo.Abp.DependencyInjection; namespace Unity.GrantManager.Intakes; @@ -9,15 +10,15 @@ namespace Unity.GrantManager.Intakes; public class ChefsFileAttachmentStreamProvider( IChefsAttachmentDownloadService chefsAttachmentDownloadService, ILogger logger) - : IChefsFileAttachmentStreamProvider, ITransientDependency + : IAttachmentContentProvider, ITransientDependency { - public async Task OpenAsync(Guid formSubmissionId, Guid chefsFileAttachmentId, string name) + + public async Task OpenAttachmentAsync(Guid formSubmissionId, Guid chefsFileAttachmentId, string name) { try { var file = await chefsAttachmentDownloadService.DownloadAsync(formSubmissionId, chefsFileAttachmentId, name); var content = file.Content ?? []; - var stream = new MemoryStream(content, writable: false); logger.LogInformation( "Opened CHEFS attachment {ChefsFileAttachmentId} for submission {FormSubmissionId}. ContentType: {ContentType}; DownloadedLength: {DownloadedLength}.", @@ -26,7 +27,9 @@ public async Task OpenAsync(Guid formSubmissionId, Gu file.ContentType, content.Length); - return new ChefsFileAttachmentStream(stream, file.ContentType); + return new AttachmentContentStream( + new MemoryStream(content, writable: false), + file.ContentType); } catch (Exception ex) { diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs index d2ba60ac1..566a31075 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Operations/AttachmentSummaryServiceTests.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Unity.AI; +using Unity.AI.Attachments; using Unity.AI.Domain; using Unity.AI.Extraction; using Unity.AI.Localization; @@ -20,7 +21,6 @@ using Unity.AI.Requests; using Unity.AI.Responses; using Unity.GrantManager.Applications; -using Unity.GrantManager.Intakes; using Volo.Abp.Domain.Repositories; using Volo.Abp; using Volo.Abp.Uow; @@ -42,9 +42,9 @@ public async Task GenerateAndSaveAsync_Uses_Streamed_Attachment_Text() var persistence = CreatePersistence(attachmentId, "test.txt", submissionId, fileId, savedSummary: summary => savedSummary = summary); - var streamProvider = Substitute.For(); - streamProvider.OpenAsync(submissionId, fileId, "test.txt") - .Returns(new ChefsFileAttachmentStream(stream, "text/plain")); + var streamProvider = Substitute.For(); + streamProvider.OpenAttachmentAsync(submissionId, fileId, "test.txt") + .Returns(new AttachmentContentStream(stream, "text/plain")); var textExtractionService = Substitute.For(); textExtractionService.ExtractTextAsync("test.txt", stream, "text/plain") @@ -84,9 +84,9 @@ public async Task GenerateAndSaveAsync_Should_Propagate_Cancellation() var persistence = CreatePersistence(attachmentId, "test.txt", submissionId, fileId); - var streamProvider = Substitute.For(); - streamProvider.OpenAsync(submissionId, fileId, "test.txt") - .Returns(new ChefsFileAttachmentStream(stream, "text/plain")); + var streamProvider = Substitute.For(); + streamProvider.OpenAttachmentAsync(submissionId, fileId, "test.txt") + .Returns(new AttachmentContentStream(stream, "text/plain")); var service = CreateService( persistence, @@ -104,7 +104,7 @@ public async Task GenerateAndSaveAsync_Should_Reject_Empty_Attachment_List() var persistence = Substitute.For(); var service = CreateService( persistence, - Substitute.For(), + Substitute.For(), Substitute.For(), Substitute.For()); @@ -124,9 +124,9 @@ public async Task GenerateAndSaveAsync_Should_Not_Call_AI_When_Supported_File_Ex var persistence = CreatePersistence(attachmentId, "test.docx", submissionId, fileId, summary => savedSummary = summary); - var streamProvider = Substitute.For(); - streamProvider.OpenAsync(submissionId, fileId, "test.docx") - .Returns(new ChefsFileAttachmentStream(stream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")); + var streamProvider = Substitute.For(); + streamProvider.OpenAttachmentAsync(submissionId, fileId, "test.docx") + .Returns(new AttachmentContentStream(stream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")); var textExtractionService = Substitute.For(); textExtractionService.ExtractTextAsync("test.docx", stream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document") @@ -157,9 +157,9 @@ public async Task GenerateAndSaveAsync_Should_Pass_Extracted_Docx_Text_To_AI() var persistence = CreatePersistence(attachmentId, "riverside-profile.docx", submissionId, fileId); - var streamProvider = Substitute.For(); - streamProvider.OpenAsync(submissionId, fileId, "riverside-profile.docx") - .Returns(new ChefsFileAttachmentStream(stream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")); + var streamProvider = Substitute.For(); + streamProvider.OpenAttachmentAsync(submissionId, fileId, "riverside-profile.docx") + .Returns(new AttachmentContentStream(stream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")); var aiService = Substitute.For(); aiService.GenerateAttachmentSummaryAsync(Arg.Do(request => capturedRequest = request)) @@ -192,9 +192,9 @@ public async Task GenerateAndSaveAsync_Should_Pass_Extracted_Text_From_Text_Atta var persistence = CreatePersistence(attachmentId, "mock-attachment.txt", submissionId, fileId); - var streamProvider = Substitute.For(); - streamProvider.OpenAsync(submissionId, fileId, "mock-attachment.txt") - .Returns(new ChefsFileAttachmentStream(stream, "text/plain")); + var streamProvider = Substitute.For(); + streamProvider.OpenAttachmentAsync(submissionId, fileId, "mock-attachment.txt") + .Returns(new AttachmentContentStream(stream, "text/plain")); var aiService = Substitute.For(); aiService.GenerateAttachmentSummaryAsync(Arg.Do(request => capturedRequest = request)) @@ -226,9 +226,9 @@ public async Task GenerateAndSaveAsync_Should_Pass_Extracted_Pdf_Text_To_AI() var persistence = CreatePersistence(attachmentId, "mock-attachment.pdf", submissionId, fileId); - var streamProvider = Substitute.For(); - streamProvider.OpenAsync(submissionId, fileId, "mock-attachment.pdf") - .Returns(new ChefsFileAttachmentStream(stream, "application/pdf")); + var streamProvider = Substitute.For(); + streamProvider.OpenAttachmentAsync(submissionId, fileId, "mock-attachment.pdf") + .Returns(new AttachmentContentStream(stream, "application/pdf")); var aiService = Substitute.For(); aiService.GenerateAttachmentSummaryAsync(Arg.Do(request => capturedRequest = request)) @@ -250,7 +250,7 @@ public async Task GenerateAndSaveAsync_Should_Pass_Extracted_Pdf_Text_To_AI() private static AttachmentSummaryService CreateService( IAttachmentSummaryDataProvider persistence, - IChefsFileAttachmentStreamProvider streamProvider, + IAttachmentContentProvider streamProvider, ITextExtractionService textExtractionService, IAIService aiService) { diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs index f19c2467c..e431a1c03 100644 --- a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/GrantApplications/Automation/AIGenerationAppServiceTests.cs @@ -36,7 +36,7 @@ public async Task GenerateAttachmentSummariesAsync_Should_Validate_Against_Appli var service = new AIGenerationAppService( Substitute.For(), - Substitute.For(), + Substitute.For(), featureGuard, Substitute.For()); service.LazyServiceProvider = GetRequiredService(); @@ -60,14 +60,14 @@ public async Task GetStatusAsync_Should_Map_Request_And_Rate_Limit_State() var operationId = Guid.NewGuid(); var requestId = Guid.NewGuid(); - var statusService = Substitute.For(); - statusService.GetLatestAsync(applicationId, operationType, tenantId).Returns(new AIGenerationRequestDto + var statusService = Substitute.For(); + statusService.GetLatestAsync(applicationId, operationType, tenantId).Returns(new Unity.AI.Generation.AIGenerationRequestDto { Id = requestId, ApplicationId = applicationId, OperationId = operationId, OperationType = operationType, - Status = AIGenerationRequestStatus.Running, + Status = AIGenerationRequestStatus.Running.ToString(), StartedAt = new DateTime(2026, 7, 1, 12, 0, 0), FailureReason = "not used", IsActive = true @@ -109,7 +109,7 @@ public async Task GetStatusAsync_Should_Reject_Unsupported_Operation_Type() { var service = new AIGenerationAppService( Substitute.For(), - Substitute.For(), + Substitute.For(), CreateFeatureGuard(), Substitute.For()); service.LazyServiceProvider = GetRequiredService(); From c75b838fdea1121c3577a00ddad5be1a12f9496e Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Tue, 4 Aug 2026 15:47:59 -0700 Subject: [PATCH 4/5] AB#33799 remove redundant attachment stream fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AI/Operations/AttachmentSummaryService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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/AI/Operations/AttachmentSummaryService.cs index db433f7ab..c65e44ac8 100644 --- 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/AI/Operations/AttachmentSummaryService.cs @@ -166,8 +166,7 @@ private async Task OpenAttachmentStreamAsync( try { cancellationToken.ThrowIfCancellationRequested(); - var stream = await attachmentContentProvider.OpenAttachmentAsync(submissionId, fileId, fileName); - return stream ?? AttachmentContentStream.Empty; + return await attachmentContentProvider.OpenAttachmentAsync(submissionId, fileId, fileName); } catch (OperationCanceledException) { From 649a1a32f37ea6b1dd24cf900d79d0100740de10 Mon Sep 17 00:00:00 2001 From: JamesPasta Date: Fri, 7 Aug 2026 09:23:48 -0700 Subject: [PATCH 5/5] feature/AB#32293-DBMigratorLogging --- .../GrantManagerEntityFrameworkCoreModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerEntityFrameworkCoreModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerEntityFrameworkCoreModule.cs index 2a48fe23a..f351815cc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerEntityFrameworkCoreModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/EntityFrameworkCore/GrantManagerEntityFrameworkCoreModule.cs @@ -60,7 +60,7 @@ public override void ConfigureServices(ServiceConfigurationContext context) options.Configure(dbContextConfiguration => { dbContextConfiguration.UseNpgsql(); - dbContextConfiguration.DbContextOptions.LogTo(Console.WriteLine, LogLevel.Information); + dbContextConfiguration.DbContextOptions.LogTo(Console.WriteLine, LogLevel.Error); }); }); }