Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,5 @@ namespace Unity.GrantManager.Attachments;

public interface IAttachmentSummaryAppService : IApplicationService
{
Task<AttachmentSummaryResultDto> GenerateAttachmentSummaryAsync(Guid attachmentId, string? promptVersion = null);
Task<List<AttachmentSummaryResultDto>> GenerateAttachmentSummariesAsync(List<Guid> attachmentIds, string? promptVersion = null);
Task<List<AttachmentSummaryResultDto>> GenerateAttachmentSummariesForPipelineAsync(List<Guid> attachmentIds, string? promptVersion = null);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Unity.AI.Automation;

public interface IApplicationAIGenerationQueue
{
Task QueueAttachmentSummaryAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null);
Task QueueAttachmentSummaryAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null, List<Guid>? attachmentIds = null);
Task QueueApplicationAnalysisAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null);
Task QueueApplicationScoringAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null);
Task QueueAllAIStagesAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,30 @@ private async Task<string> GenerateOrFallbackAsync(
}
}

public async Task<List<string>> GenerateForApplicationAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default)
public async Task<List<string>> GenerateForApplicationAsync(
Guid applicationId,
string? promptVersion = null,
IReadOnlyCollection<Guid>? attachmentIds = null,
CancellationToken cancellationToken = default)
{
var attachmentIds = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId))
var applicationAttachmentIds = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId))
.Select(a => a.Id)
.ToList();

return await GenerateAndSaveAsync(attachmentIds, promptVersion, cancellationToken);
if (attachmentIds is not { Count: > 0 })
{
return await GenerateAndSaveAsync(applicationAttachmentIds, promptVersion, cancellationToken);
}

var applicationAttachmentIdSet = applicationAttachmentIds.ToHashSet();
var selectedIds = attachmentIds.Distinct().ToList();

if (selectedIds.Any(id => !applicationAttachmentIdSet.Contains(id)))
{
throw new InvalidOperationException("One or more selected attachments do not belong to the application.");
}

return await GenerateAndSaveAsync(selectedIds, promptVersion, cancellationToken);
Comment thread
jacobwillsmith marked this conversation as resolved.
}

private async Task<ChefsFileAttachmentStream> OpenAttachmentStreamAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ public interface IAttachmentSummaryService
{
Task<string> GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null, CancellationToken cancellationToken = default);
Task<List<string>> GenerateAndSaveAsync(IEnumerable<Guid> attachmentIds, string? promptVersion = null, CancellationToken cancellationToken = default);
Task<List<string>> GenerateForApplicationAsync(Guid applicationId, string? promptVersion = null, CancellationToken cancellationToken = default);
Task<List<string>> GenerateForApplicationAsync(Guid applicationId, string? promptVersion = null, IReadOnlyCollection<Guid>? attachmentIds = null, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.AI;
using Unity.AI.Features;
using Unity.AI.Localization;
using Unity.AI.Operations;
using Unity.AI.Permissions;
using Unity.AI.Settings;
Expand All @@ -15,40 +13,8 @@ namespace Unity.GrantManager.Attachments;
[Authorize(AIPermissions.Analysis.GenerateAttachmentSummaries)]
[ExposeServices(typeof(AttachmentSummaryAppService), typeof(IAttachmentSummaryAppService))]
public class AttachmentSummaryAppService(
IAttachmentSummaryService attachmentSummaryService,
AIFeatureGuard featureGuard) : AIAppService, IAttachmentSummaryAppService
IAttachmentSummaryService attachmentSummaryService) : AIAppService, IAttachmentSummaryAppService
{
public virtual async Task<AttachmentSummaryResultDto> GenerateAttachmentSummaryAsync(System.Guid attachmentId, string? promptVersion = null)
{
await featureGuard.EnsureEnabledAsync(
AIFeatures.AttachmentSummaries,
AILocalizationKeys.AttachmentSummariesDisabled);

await attachmentSummaryService.GenerateAndSaveAsync(attachmentId, promptVersion);
return new AttachmentSummaryResultDto { Completed = true };
}

public virtual async Task<List<AttachmentSummaryResultDto>> GenerateAttachmentSummariesAsync(List<System.Guid> attachmentIds, string? promptVersion = null)
{
await featureGuard.EnsureEnabledAsync(
AIFeatures.AttachmentSummaries,
AILocalizationKeys.AttachmentSummariesDisabled);

if (attachmentIds.Count == 0)
{
return [];
}

var results = new List<AttachmentSummaryResultDto>();
foreach (var attachmentId in attachmentIds)
{
await attachmentSummaryService.GenerateAndSaveAsync(attachmentId, promptVersion);
results.Add(new AttachmentSummaryResultDto { Completed = true });
}

return results;
}

// Internal-only: no HTTP endpoint, no auth check — safe for background job callers
[AllowAnonymous]
[RemoteService(IsEnabled = false)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;

namespace Unity.GrantManager.GrantApplications.Automation.BackgroundJobs;

public class GenerateAttachmentSummaryBackgroundJobArgs
{
public Guid ApplicationId { get; set; }
public Guid? TenantId { get; set; }
public Guid? RequestedByUserId { get; set; }
public List<Guid>? AttachmentIds { get; set; }
public string? PromptVersion { get; set; }
public string RequestKey { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public interface IGrantApplicationAppService
Task<GrantApplicationDto> TriggerAction(Guid applicationId, GrantApplicationAction triggerAction);
Task<AIGenerationRequestDto> QueueAIGenerationAsync(Guid applicationId, string? promptVersion = null);
Task<AIGenerationRequestDto> QueueApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null);
Task<AIGenerationRequestDto> QueueAttachmentSummaryAsync(Guid applicationId, string? promptVersion = null);
Task<AIGenerationRequestDto> QueueAttachmentSummaryAsync(QueueAttachmentSummaryRequestDto input, string? promptVersion = null);
Task<AIGenerationRequestDto> QueueApplicationScoringAsync(Guid applicationId, string? promptVersion = null);
Task<AIGenerationRequestDto?> GetAIGenerationStatusAsync(Guid applicationId, string operationType, string? promptVersion = null);
Task<AIGenerationRequestDto> QueueAllAIStagesAsync(Guid applicationId, string? promptVersion = null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;

namespace Unity.GrantManager.GrantApplications;

public class QueueAttachmentSummaryRequestDto
{
public Guid ApplicationId { get; set; }
public List<Guid>? AttachmentIds { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Unity.AI.Automation;
Expand All @@ -23,7 +24,7 @@ public class ApplicationAIGenerationQueue(
ILogger<ApplicationAIGenerationQueue> logger)
: IApplicationAIGenerationQueue, ITransientDependency
{
public async Task QueueAttachmentSummaryAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null)
public async Task QueueAttachmentSummaryAsync(Guid applicationId, Guid? tenantId, string? promptVersion = null, List<Guid>? attachmentIds = null)
{
var requestKey = AIGenerationRequestKeyHelper.BuildRequestKey(tenantId, applicationId, AIGenerationRequestKeyHelper.AttachmentSummaryOperationType);
await EnsureRequestAndEnqueueAsync(
Expand All @@ -36,6 +37,7 @@ await EnsureRequestAndEnqueueAsync(
return backgroundJobManager.EnqueueAsync(new GenerateAttachmentSummaryBackgroundJobArgs
{
ApplicationId = applicationId,
AttachmentIds = attachmentIds,
PromptVersion = promptVersion,
RequestedByUserId = currentUser.Id,
TenantId = tenantId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public override async Task ExecuteAsync(GenerateApplicationAnalysisBackgroundJob
await applicationAnalysisService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion);
logger.LogInformation("Completed AI application analysis job for application {ApplicationId}.", args.ApplicationId);

await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey);
await AIGenerationRequestJobHelper.StampRateLimitBestEffortAsync(aiRateLimiter, logger, args.RequestedByUserId, args.ApplicationId, args.RequestKey);
await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent
}
logger.LogInformation("Completed AI application scoring job for application {ApplicationId}.", args.ApplicationId);

await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey);
await AIGenerationRequestJobHelper.StampRateLimitBestEffortAsync(aiRateLimiter, logger, args.RequestedByUserId, args.ApplicationId, args.RequestKey);
await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ public override async Task ExecuteAsync(GenerateAttachmentSummaryBackgroundJobAr
logger.LogInformation(
"Executing AI attachment summary job for application {ApplicationId}.",
args.ApplicationId);
await attachmentSummaryService.GenerateForApplicationAsync(args.ApplicationId, args.PromptVersion);
await attachmentSummaryService.GenerateForApplicationAsync(args.ApplicationId, args.PromptVersion, args.AttachmentIds);

await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey);
await AIGenerationRequestJobHelper.StampRateLimitBestEffortAsync(aiRateLimiter, logger, args.RequestedByUserId, args.ApplicationId, args.RequestKey);
await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public override async Task ExecuteAsync(RunApplicationAIPipelineJobArgs args)
if (!attachmentSummariesEnabled && !applicationAnalysisEnabled && !scoringEnabled)
{
logger.LogDebug("All AI features are disabled, skipping queued AI generation for application {ApplicationId}.", args.ApplicationId);
await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey);
await AIGenerationRequestJobHelper.StampRateLimitBestEffortAsync(aiRateLimiter, logger, args.RequestedByUserId, args.ApplicationId, args.RequestKey);
await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey);
return;
}

Expand Down Expand Up @@ -115,8 +115,8 @@ await localEventBus.PublishAsync(new ApplicationAIScoringGeneratedEvent
throw analysisException;
}

await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey);
await AIGenerationRequestJobHelper.StampRateLimitBestEffortAsync(aiRateLimiter, logger, args.RequestedByUserId, args.ApplicationId, args.RequestKey);
await AIGenerationRequestJobHelper.MarkCompletedInNewUowAsync(unitOfWorkManager, generationRequestRepository, args.RequestKey);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ namespace Unity.GrantManager.GrantApplications;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(GrantApplicationAppService), typeof(IGrantApplicationAppService))]
public class GrantApplicationAppService(
IApplicationManager applicationManager,
IApplicationRepository applicationRepository,
IApplicationStatusRepository applicationStatusRepository,
IApplicationManager applicationManager,
IApplicationRepository applicationRepository,
IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository,
IApplicationStatusRepository applicationStatusRepository,
IApplicationFormSubmissionRepository applicationFormSubmissionRepository,
IApplicantRepository applicantRepository,
IApplicationFormRepository applicationFormRepository,
Expand Down Expand Up @@ -1183,15 +1184,16 @@ public async Task<AIGenerationRequestDto> QueueApplicationAnalysisAsync(Guid app
}

[Authorize(AIPermissions.Analysis.GenerateAttachmentSummaries)]
public async Task<AIGenerationRequestDto> QueueAttachmentSummaryAsync(Guid applicationId, string? promptVersion = null)
{
await EnsureAttachmentSummariesEnabledAsync();
await aiGenerationQueue.QueueAttachmentSummaryAsync(applicationId, CurrentTenant.Id, promptVersion);

var request = await aiGenerationStatusAppService.GetLatestAsync(
applicationId,
AIGenerationRequestKeyHelper.AttachmentSummaryOperationType,
CurrentTenant.Id);
public async Task<AIGenerationRequestDto> QueueAttachmentSummaryAsync(QueueAttachmentSummaryRequestDto input, string? promptVersion = null)
{
await EnsureAttachmentSummariesEnabledAsync();
var attachmentIds = await ResolveAttachmentSummaryIdsAsync(input);
await aiGenerationQueue.QueueAttachmentSummaryAsync(input.ApplicationId, CurrentTenant.Id, promptVersion, attachmentIds);

var request = await aiGenerationStatusAppService.GetLatestAsync(
input.ApplicationId,
AIGenerationRequestKeyHelper.AttachmentSummaryOperationType,
CurrentTenant.Id);

return request ?? throw new UserFriendlyException("Unable to queue AI attachment summary request.");
}
Expand Down Expand Up @@ -1257,13 +1259,49 @@ private async Task EnsureAIGenerationStatusAccessAsync(string operationType)
}
}

private async Task EnsureAttachmentSummariesEnabledAsync()
{
if (!await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries"))
{
throw new UserFriendlyException("AI attachment summaries are not enabled.");
}
}
private async Task EnsureAttachmentSummariesEnabledAsync()
{
if (!await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries"))
{
throw new UserFriendlyException("AI attachment summaries are not enabled.");
}
}

private async Task<List<Guid>> ResolveAttachmentSummaryIdsAsync(QueueAttachmentSummaryRequestDto input)
{
if (input == null)
{
throw new UserFriendlyException("Attachment summary request is required.");
}

if (input.ApplicationId == Guid.Empty)
{
throw new UserFriendlyException("Application id is required.");
}

var applicationAttachmentIds = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == input.ApplicationId))
.Select(a => a.Id)
.ToList();

if (applicationAttachmentIds.Count == 0)
{
throw new UserFriendlyException("No attachments were found to generate summaries.");
}

if (input.AttachmentIds is not { Count: > 0 })
{
return applicationAttachmentIds;
}

var applicationAttachmentIdSet = applicationAttachmentIds.ToHashSet();
var selectedAttachmentIds = input.AttachmentIds.Distinct().ToList();
if (selectedAttachmentIds.Any(id => !applicationAttachmentIdSet.Contains(id)))
{
throw new UserFriendlyException("One or more selected attachments do not belong to the application.");
}

return selectedAttachmentIds;
}

private async Task EnsureAIAnalysisEnabledAsync()
{
Expand Down Expand Up @@ -1424,4 +1462,4 @@ private static void UpdateFindingDismissedState(IEnumerable<ApplicationAnalysisF
return null;
}
}
}
}
Loading
Loading