Skip to content
Merged

Dev #2434

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
63901e7
Column index generation created across main tables to support the Gra…
DavidBrightBcGov May 7, 2026
1a7b57c
Merge branch 'dev' into feature/AB#32084-app-list-performance-indexes
DavidBrightBcGov May 7, 2026
9fb6053
AB#32290 throttle AI analysis with per-user 60s cooldown
jacobwillsmith Apr 30, 2026
837bead
AB#32290 simplify AI generation throttling
jacobwillsmith May 2, 2026
6581544
AB#32290 start cooldown after AI generation completes
jacobwillsmith May 5, 2026
5d6eb6e
AB#32290 clean up AI generation cooldown flow
jacobwillsmith May 6, 2026
8d4db65
AB#32290 carry requester through AI jobs
jacobwillsmith May 6, 2026
d80943c
AB#32290 harden AI generation cooldown
jacobwillsmith May 7, 2026
40249eb
AB#32892 centralize AI feature-disabled checks
jacobwillsmith May 1, 2026
dcc2d6e
AB#32890 remove dead AI completion code path
jacobwillsmith May 1, 2026
e63dd94
Optimized the main GrantApplications table to only pull necessary col…
DavidBrightBcGov May 7, 2026
330f19c
AB#32545: Update Assessment Buttons Styling, Order, and Text
aurelio-aot May 7, 2026
7764f6b
Merge pull request #2409 from bcgov/feature/AB#32290-throttle-ai-anal…
JamesPasta May 7, 2026
120b053
Merge branch 'dev' into bugfix/AB#32545-AssessmentTable-Text-ButtonsS…
aurelio-aot May 7, 2026
d372c91
AB#32683 report history mapping
AndreGAot May 7, 2026
7406cfe
AB#32903 simplify AI provider payload validator helpers
jacobwillsmith May 1, 2026
fb2fcdc
AB#32898 consolidate AI JSON serializer defaults
jacobwillsmith May 1, 2026
9536fac
Merge pull request #2430 from bcgov/bugfix/AB#32683-applicanthistory-…
AndreGAot May 7, 2026
28021f4
AB#32902 add AI runtime validation tests
jacobwillsmith May 1, 2026
04101fb
Merge pull request #2421 from bcgov/feature/AB#32084-app-list-perform…
DavidBrightBcGov May 8, 2026
5d7c21f
Apply suggestions from code review
DavidBrightBcGov May 8, 2026
d9f4c07
Merge pull request #2425 from bcgov/feature/AB#32688-app-list-perform…
DavidBrightBcGov May 8, 2026
73ad3f8
Pre-warm EF Core, DataTable perf, config & secrets update
DavidBrightBcGov May 8, 2026
adc0669
Merge branch 'dev' into feature/AB#32781-app-list-performance-code-up…
DavidBrightBcGov May 8, 2026
a014b5a
Apply suggestions from code review
DavidBrightBcGov May 8, 2026
110e110
As per Copilot flag, added configurable DB warmup: options, limits, a…
DavidBrightBcGov May 8, 2026
11c53ba
Merge pull request #2432 from bcgov/feature/AB#32781-app-list-perform…
DavidBrightBcGov May 8, 2026
a01fe02
Merge pull request #2427 from bcgov/bugfix/AB#32545-AssessmentTable-T…
JamesPasta May 8, 2026
7fea040
AB#32543 move AI reporting into AI module
jacobwillsmith May 8, 2026
3731840
Merge pull request #2423 from bcgov/feature/AB#32890-remove-dead-ai-c…
JamesPasta May 8, 2026
e4c283e
Merge pull request #2431 from bcgov/feature/AB#32903-ai-provider-payl…
JamesPasta May 8, 2026
fd76048
Merge pull request #2433 from bcgov/feature/AB#32543-integrate-ai-rep…
JamesPasta May 8, 2026
5aa6f2c
Merge pull request #2429 from bcgov/feature/AB#32902-ai-runtime-valid…
JamesPasta May 8, 2026
dd1d005
Merge pull request #2424 from bcgov/feature/AB#32892-centralize-ai-fe…
JamesPasta May 8, 2026
8cce699
Merge pull request #2426 from bcgov/feature/AB#32898-consolidate-ai-j…
JamesPasta May 8, 2026
d655cba
AB#32900 make AI generation runtime cancellation-ready
jacobwillsmith May 1, 2026
ee662a0
Merge pull request #2428 from bcgov/feature/AB#32900-ai-generation-ru…
JamesPasta May 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
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Unity.AI.Extraction
{
public interface ITextExtractionService
{
Task<string> ExtractTextAsync(string fileName, Stream fileContent, string contentType);
Task<string> ExtractTextAsync(string fileName, Stream fileContent, string contentType, CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
using Unity.AI.Requests;
using Unity.AI.Responses;
Expand All @@ -8,9 +9,8 @@ public interface IAIService
{
Task<bool> IsAvailableAsync();

Task<AICompletionResponse> GenerateCompletionAsync(AICompletionRequest request);
Task<AttachmentSummaryResponse> GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request);
Task<ApplicationAnalysisResponse> GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request);
Task<ApplicationScoringResponse> GenerateApplicationScoringAsync(ApplicationScoringRequest request);
Task<AttachmentSummaryResponse> GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request, CancellationToken cancellationToken = default);
Task<ApplicationAnalysisResponse> GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request, CancellationToken cancellationToken = default);
Task<ApplicationScoringResponse> GenerateApplicationScoringAsync(ApplicationScoringRequest request, CancellationToken cancellationToken = default);
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Unity.AI.RateLimit;

public class AIRateLimitStateDto
{
public int RetryAfterSeconds { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Threading.Tasks;
using Volo.Abp.Application.Services;

namespace Unity.AI.RateLimit;

public interface IAIRateLimitAppService : IApplicationService
{
Task<AIRateLimitStateDto> GetStateAsync();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Threading.Tasks;
using System;

namespace Unity.AI.RateLimit;

public interface IAIRateLimiter
{
/// <summary>
/// Throws <see cref="Volo.Abp.UserFriendlyException"/> if the current user is still
/// inside their AI generate cooldown window.
/// </summary>
Task EnsureAsync();

/// <summary>
/// Starts a fresh cooldown for the current user. No-op for callers without a user.
/// </summary>
Task StampAsync();

/// <summary>
/// Starts a fresh cooldown for the supplied user. No-op when userId is null.
/// </summary>
Task StampAsync(Guid? userId);

/// <summary>
/// Returns the remaining cooldown for the current user. RetryAfterSeconds is 0 when
/// the user can generate immediately.
/// </summary>
Task<AIRateLimitStateDto> GetStateAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using UglyToad.PdfPig;
Expand All @@ -32,7 +33,7 @@ public TextExtractionService(ILogger<TextExtractionService> logger)
_logger = logger;
}

public Task<string> ExtractTextAsync(string fileName, Stream fileContent, string contentType)
public Task<string> ExtractTextAsync(string fileName, Stream fileContent, string contentType, CancellationToken cancellationToken = default)
{
if (fileContent == null)
{
Expand All @@ -42,6 +43,7 @@ public Task<string> ExtractTextAsync(string fileName, Stream fileContent, string

try
{
cancellationToken.ThrowIfCancellationRequested();
var normalizedContentType = contentType?.ToLowerInvariant() ?? string.Empty;
var extension = Path.GetExtension(fileName)?.ToLowerInvariant() ?? string.Empty;

Expand All @@ -53,12 +55,12 @@ public Task<string> ExtractTextAsync(string fileName, Stream fileContent, string

var rawText = extension switch
{
".txt" or ".csv" or ".json" or ".xml" => ExtractTextFromTextFile(fileContent),
".pdf" => ExtractTextFromPdfFile(fileName, fileContent),
".docx" => ExtractTextFromWordDocx(fileName, fileContent),
".xls" or ".xlsx" => ExtractTextFromExcelFile(fileName, fileContent),
".pptx" => ExtractTextFromPowerPointFile(fileName, fileContent),
_ => ExtractByContentType(fileName, fileContent, normalizedContentType)
".txt" or ".csv" or ".json" or ".xml" => ExtractTextFromTextFile(fileContent, cancellationToken),
".pdf" => ExtractTextFromPdfFile(fileName, fileContent, cancellationToken),
".docx" => ExtractTextFromWordDocx(fileName, fileContent, cancellationToken),
".xls" or ".xlsx" => ExtractTextFromExcelFile(fileName, fileContent, cancellationToken),
".pptx" => ExtractTextFromPowerPointFile(fileName, fileContent, cancellationToken),
_ => ExtractByContentType(fileName, fileContent, normalizedContentType, cancellationToken)
};

if (string.IsNullOrEmpty(rawText))
Expand All @@ -69,36 +71,44 @@ public Task<string> ExtractTextAsync(string fileName, Stream fileContent, string

return Task.FromResult(NormalizeAndLimitText(rawText, fileName));
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error extracting text from {FileName}", fileName);
return Task.FromResult(string.Empty);
}
}

private string ExtractByContentType(string fileName, Stream fileContent, string normalizedContentType)
private string ExtractByContentType(
string fileName,
Stream fileContent,
string normalizedContentType,
CancellationToken cancellationToken)
{
if (normalizedContentType.Contains("text/"))
{
return ExtractTextFromTextFile(fileContent);
return ExtractTextFromTextFile(fileContent, cancellationToken);
}
if (normalizedContentType.Contains("pdf"))
{
return ExtractTextFromPdfFile(fileName, fileContent);
return ExtractTextFromPdfFile(fileName, fileContent, cancellationToken);
}
if (normalizedContentType.Contains("word") ||
normalizedContentType.Contains("msword") ||
normalizedContentType.Contains("officedocument.wordprocessingml"))
{
return ExtractTextFromWordDocx(fileName, fileContent);
return ExtractTextFromWordDocx(fileName, fileContent, cancellationToken);
}
if (normalizedContentType.Contains("excel") || normalizedContentType.Contains("spreadsheet"))
{
return ExtractTextFromExcelFile(fileName, fileContent);
return ExtractTextFromExcelFile(fileName, fileContent, cancellationToken);
}
if (normalizedContentType.Contains("presentation") || normalizedContentType.Contains("powerpoint"))
{
return ExtractTextFromPowerPointFile(fileName, fileContent);
return ExtractTextFromPowerPointFile(fileName, fileContent, cancellationToken);
}
return string.Empty;
}
Expand All @@ -111,7 +121,7 @@ private static void RewindIfPossible(Stream stream)
}
}

private string ExtractTextFromTextFile(Stream fileContent)
private string ExtractTextFromTextFile(Stream fileContent, CancellationToken cancellationToken)
{
try
{
Expand All @@ -122,6 +132,7 @@ private string ExtractTextFromTextFile(Stream fileContent)
int read;
while ((read = reader.Read(buffer, 0, buffer.Length)) > 0)
{
cancellationToken.ThrowIfCancellationRequested();
var remaining = MaxExtractedTextLength - builder.Length;
if (remaining <= 0) break;
builder.Append(buffer, 0, Math.Min(read, remaining));
Expand All @@ -142,7 +153,7 @@ private string ExtractTextFromTextFile(Stream fileContent)
}
}

private string ExtractTextFromPdfFile(string fileName, Stream fileContent)
private string ExtractTextFromPdfFile(string fileName, Stream fileContent, CancellationToken cancellationToken)
{
try
{
Expand All @@ -156,6 +167,7 @@ private string ExtractTextFromPdfFile(string fileName, Stream fileContent)

foreach (var pageText in pageTexts)
{
cancellationToken.ThrowIfCancellationRequested();
if (builder.Length >= MaxExtractedTextLength)
{
break;
Expand All @@ -178,15 +190,15 @@ private string ExtractTextFromPdfFile(string fileName, Stream fileContent)
}
}

private string ExtractTextFromWordDocx(string fileName, Stream fileContent)
private string ExtractTextFromWordDocx(string fileName, Stream fileContent, CancellationToken cancellationToken)
{
try
{
RewindIfPossible(fileContent);
using var document = new XWPFDocument(fileContent);
var builder = new StringBuilder();
var processedParagraphCount = AppendDocxParagraphText(document, builder);
var processedTableRowCount = AppendDocxTableText(document, builder);
var processedParagraphCount = AppendDocxParagraphText(document, builder, cancellationToken);
var processedTableRowCount = AppendDocxTableText(document, builder, cancellationToken);

_logger.LogDebug(
"Extracted Word text from {ProcessedParagraphCount} paragraphs and {ProcessedTableRowCount} table rows for {FileName}",
Expand All @@ -202,7 +214,10 @@ private string ExtractTextFromWordDocx(string fileName, Stream fileContent)
}
}

private static int AppendDocxParagraphText(XWPFDocument document, StringBuilder builder)
private static int AppendDocxParagraphText(
XWPFDocument document,
StringBuilder builder,
CancellationToken cancellationToken)
{
var processedParagraphCount = 0;
var paragraphTexts = document.Paragraphs
Expand All @@ -212,6 +227,7 @@ private static int AppendDocxParagraphText(XWPFDocument document, StringBuilder

foreach (var paragraphText in paragraphTexts)
{
cancellationToken.ThrowIfCancellationRequested();
if (builder.Length >= MaxExtractedTextLength)
{
break;
Expand All @@ -227,7 +243,10 @@ private static int AppendDocxParagraphText(XWPFDocument document, StringBuilder
return processedParagraphCount;
}

private static int AppendDocxTableText(XWPFDocument document, StringBuilder builder)
private static int AppendDocxTableText(
XWPFDocument document,
StringBuilder builder,
CancellationToken cancellationToken)
{
if (builder.Length >= MaxExtractedTextLength)
{
Expand All @@ -237,8 +256,10 @@ private static int AppendDocxTableText(XWPFDocument document, StringBuilder buil
var processedTableRowCount = 0;
foreach (var table in document.Tables)
{
cancellationToken.ThrowIfCancellationRequested();
foreach (var row in table.Rows.Take(MaxDocxTableRows))
{
cancellationToken.ThrowIfCancellationRequested();
if (builder.Length >= MaxExtractedTextLength)
{
return processedTableRowCount;
Expand All @@ -252,6 +273,7 @@ private static int AppendDocxTableText(XWPFDocument document, StringBuilder buil
var rowHadValue = false;
foreach (var cellText in cellTexts)
{
cancellationToken.ThrowIfCancellationRequested();
rowHadValue = true;
if (TryAppendWithTrailingNewline(builder, cellText))
{
Expand All @@ -269,7 +291,7 @@ private static int AppendDocxTableText(XWPFDocument document, StringBuilder buil
return processedTableRowCount;
}

private string ExtractTextFromExcelFile(string fileName, Stream fileContent)
private string ExtractTextFromExcelFile(string fileName, Stream fileContent, CancellationToken cancellationToken)
{
try
{
Expand All @@ -282,13 +304,14 @@ private string ExtractTextFromExcelFile(string fileName, Stream fileContent)

for (var sheetIndex = 0; sheetIndex < sheetCount; sheetIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
if (builder.Length >= MaxExtractedTextLength)
{
break;
}

var sheet = workbook.GetSheetAt(sheetIndex);
var (rowsProcessed, limitReached) = TryAppendExcelSheet(sheet, builder);
var (rowsProcessed, limitReached) = TryAppendExcelSheet(sheet, builder, cancellationToken);
if (rowsProcessed > 0)
{
processedSheetCount++;
Expand All @@ -315,7 +338,7 @@ private string ExtractTextFromExcelFile(string fileName, Stream fileContent)
}
}

private string ExtractTextFromPowerPointFile(string fileName, Stream fileContent)
private string ExtractTextFromPowerPointFile(string fileName, Stream fileContent, CancellationToken cancellationToken)
{
try
{
Expand All @@ -328,6 +351,7 @@ private string ExtractTextFromPowerPointFile(string fileName, Stream fileContent

foreach (var slideEntry in slideEntries)
{
cancellationToken.ThrowIfCancellationRequested();
if (builder.Length >= MaxExtractedTextLength)
{
break;
Expand Down Expand Up @@ -398,7 +422,10 @@ private IEnumerable<ZipArchiveEntry> GetOrderedPowerPointSlideEntries(ZipArchive
return orderedEntries;
}

private static (int RowsProcessed, bool LimitReached) TryAppendExcelSheet(ISheet? sheet, StringBuilder builder)
private static (int RowsProcessed, bool LimitReached) TryAppendExcelSheet(
ISheet? sheet,
StringBuilder builder,
CancellationToken cancellationToken)
{
if (sheet == null)
{
Expand All @@ -408,12 +435,13 @@ private static (int RowsProcessed, bool LimitReached) TryAppendExcelSheet(ISheet
var processedRows = 0;
foreach (IRow row in sheet)
{
cancellationToken.ThrowIfCancellationRequested();
if (processedRows >= MaxExcelRowsPerSheet || builder.Length >= MaxExtractedTextLength)
{
break;
}

var (rowHadValue, limitReached) = TryAppendExcelRow(row, builder);
var (rowHadValue, limitReached) = TryAppendExcelRow(row, builder, cancellationToken);
if (rowHadValue)
{
processedRows++;
Expand All @@ -428,11 +456,15 @@ private static (int RowsProcessed, bool LimitReached) TryAppendExcelSheet(ISheet
return (processedRows, builder.Length >= MaxExtractedTextLength);
}

private static (bool RowHadValue, bool LimitReached) TryAppendExcelRow(IRow row, StringBuilder builder)
private static (bool RowHadValue, bool LimitReached) TryAppendExcelRow(
IRow row,
StringBuilder builder,
CancellationToken cancellationToken)
{
var rowHasValue = false;
foreach (var cell in row.Cells.Take(MaxExcelCellsPerRow))
{
cancellationToken.ThrowIfCancellationRequested();
var value = GetCellText(cell);
if (string.IsNullOrWhiteSpace(value))
{
Expand Down
Loading
Loading