Skip to content
Merged

Dev #2781

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
47755af
[AB#33417] Fix HTML Injection in Email Comment Notifications Bug
plavoie-BC Aug 4, 2026
6496d67
[AB#33420] Fix XSS User Profile Data in Notification Settings Bug
plavoie-BC Aug 4, 2026
b264f7f
AB#33942 - Replaced old names with canonical FA7 names
hasanpour Aug 4, 2026
47ba0bd
AB#33942 - Rewrote Shim-dependent icons
hasanpour Aug 4, 2026
30982e1
AB#33942- Prefix Normalization
hasanpour Aug 4, 2026
52986e0
AB#33942 - Droped v4-shims.css
hasanpour Aug 4, 2026
86fc744
AB#33942 - Fixed icons in ABP buttons
hasanpour Aug 5, 2026
57f85a6
[AB#33417] Fix Email Comment Notifications rendering with XSS sanitiz…
plavoie-BC Aug 5, 2026
fa81f5f
[AB#33417] Fix email template reference appears in emails
plavoie-BC Aug 5, 2026
ae0d79e
[AB#33420] Strengthen Notification Settings XSS
plavoie-BC Aug 5, 2026
ec9d7cd
Merge remote-tracking branch 'origin/dev' into bugfix/AB#33420-xss-vu…
plavoie-BC Aug 5, 2026
7c15b58
[AB#33783] Secure AIPromptAppService critical endpoints
plavoie-BC Aug 6, 2026
b4353d2
[AB#33783] Secure AccountCodingAppService critical endpoints
plavoie-BC Aug 6, 2026
7e3565c
AB#33248: Scoresheet Save/Discard All Buttons - Initial Draft
aurelio-aot Aug 6, 2026
42b3e01
[AB#33783] Secure PaymentThresholdAppService critical endpoints
plavoie-BC Aug 7, 2026
de46479
[AB#33783] Secure IntakeAppService critical endpoints
plavoie-BC Aug 7, 2026
7783656
[AB#33783] Secure ApplicationLinksAppService critical endpoints
plavoie-BC Aug 7, 2026
cf12a2f
[AB#33783] Secure ApplicationFormAppService critical endpoints
plavoie-BC Aug 7, 2026
5fdba9c
[AB#33783] Secure ApplicationFormVersionAppService critical endpoints
plavoie-BC Aug 7, 2026
6351322
[AB#33783] Secure EndpointManagementAppService critical endpoints
plavoie-BC Aug 7, 2026
5da3bfc
[AB#33783] Update ApplicationLinks Comments
plavoie-BC Aug 7, 2026
2438abb
Merge remote-tracking branch 'origin/dev' into feature/AB#33783-CRUD-…
plavoie-BC Aug 7, 2026
d78448f
AB#33248: Make the disabled buttons more visible
aurelio-aot Aug 7, 2026
f456614
bugfix/AB#33961-500Exception
JamesPasta Aug 7, 2026
3f2f284
Merge pull request #2773 from bcgov/feature/AB#33783-CRUD-exposure-si…
JamesPasta Aug 7, 2026
a3f3002
Merge pull request #2765 from bcgov/bugfix/AB#33420-xss-vuln-fixes
JamesPasta Aug 7, 2026
788ea15
Merge pull request #2768 from bcgov/feature/AB#33942-Normalize-FontAw…
JamesPasta Aug 7, 2026
bdfb542
Merge remote-tracking branch 'origin/dev' into bugfix/AB#33961-500Exc…
JamesPasta Aug 7, 2026
86a44ba
AB#33981 streamline AI operation pipeline
jacobwillsmith Aug 5, 2026
87b70d8
AB#33981 address AI pipeline analyzer warnings
jacobwillsmith Aug 5, 2026
a16bfc1
AB#33981 remove stale cooldown dependency
jacobwillsmith Aug 7, 2026
b9c46d9
Merge pull request #2769 from bcgov/feature/AB#33981-ai-operation-pip…
JamesPasta Aug 7, 2026
bf0e166
AB#33248: Change Header Colors of Dirty Sections and Questions
aurelio-aot Aug 7, 2026
5678a96
AB#33984 select tenant AI prompts with global fallback
jacobwillsmith Aug 5, 2026
7d80878
AB#33248: Fix sonarqube issues
aurelio-aot Aug 7, 2026
19e41f2
bugfix/AB#33961-SonarCleanup
JamesPasta Aug 7, 2026
c4a99a6
Merge pull request #2772 from bcgov/feature/AB#33248-Scoresheet-Save-…
JamesPasta Aug 7, 2026
95c702f
Merge pull request #2778 from bcgov/bugfix/AB#33961-500ExcptionOnUnit…
JamesPasta Aug 8, 2026
2c8edce
AB#33984 address tenant prompt review findings
jacobwillsmith Aug 8, 2026
b875e28
Merge pull request #2780 from bcgov/feature/ai-tenant-prompt-overrides
JamesPasta Aug 8, 2026
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
5 changes: 5 additions & 0 deletions applications/Unity.GrantManager/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
<!-- Add direct reference to the patched Scriban to ensure all projects importing common.props
use the safe version (pins transitive consumers that do not specify a version). -->
<PackageReference Include="Scriban" Version="7.2.5" />

<!-- System.Security.Cryptography.Xml 8.0.2 (pulled in transitively) has several known high
severity vulnerabilities; pin to the same patched version already used directly by
Unity.AI.Application so every project importing common.props gets the safe version. -->
<PackageReference Include="System.Security.Cryptography.Xml" Version="10.0.10" />
</ItemGroup>

</Project>
10 changes: 5 additions & 5 deletions applications/Unity.GrantManager/modules/Unity.AI/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, AIGenerationOperationDefinition> Definitions =
new Dictionary<string, AIGenerationOperationDefinition>(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<AIGenerationOperationDefinition> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<Guid> AttachmentIds { get; set; } = [];

public string? PromptVersion { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ public interface IAIRateLimiter
/// </summary>
Task EnsureAsync();

/// <summary>
/// Throws if the supplied user is still inside their AI generate cooldown window.
/// No-op when userId is null.
/// </summary>
Task EnsureAsync(Guid? userId);

/// <summary>
/// Starts a fresh cooldown for the current user. No-op for callers without a user.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,20 +16,19 @@ namespace Unity.AI.DataSeed;
public class AIOperationDataSeeder(
IRepository<AIOperation, Guid> operationRepository,
IRepository<AIModel, Guid> modelRepository,
IRepository<AIPrompt, Guid> promptRepository,
ICurrentTenant currentTenant,
ILogger<AIOperationDataSeeder> logger) : ITransientDependency
{
private const string DefaultModelName = "Gpt5Mini";

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)
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -96,18 +83,8 @@ await operationRepository.InsertAsync(
return models.FirstOrDefault();
}

private async Task<AIPrompt?> 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ───────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ public class AIOperation : AuditedAggregateRoot<Guid>

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; }
Expand All @@ -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;
}

}
Loading
Loading