From f6a73540a2f98468c2d3885131c2479554f07133 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 17:25:33 -0800 Subject: [PATCH 1/3] AB#32010 Refactor AI contracts and add compatibility request wrappers --- .../AI/AIAttachmentPromptItem.cs | 8 +++ .../AI/AICompletionRequest.cs | 10 ++++ .../AI/AIJsonKeys.cs | 20 +++++++ .../AI/ApplicationAnalysisRequest.cs | 19 +++++++ .../AI/ApplicationAnalysisResponse.cs | 35 ++++++++++++ .../AI/AttachmentSummaryRequest.cs | 9 ++++ .../AI/IAIService.cs | 7 +++ .../AI/ScoresheetSectionRequest.cs | 13 +++++ .../AI/OpenAIService.cs | 54 +++++++++++++++++++ 9 files changed, 175 insertions(+) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIAttachmentPromptItem.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AICompletionRequest.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisRequest.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisResponse.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AttachmentSummaryRequest.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ScoresheetSectionRequest.cs diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIAttachmentPromptItem.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIAttachmentPromptItem.cs new file mode 100644 index 000000000..2918f03e8 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIAttachmentPromptItem.cs @@ -0,0 +1,8 @@ +namespace Unity.GrantManager.AI +{ + public class AIAttachmentPromptItem + { + public string Name { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AICompletionRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AICompletionRequest.cs new file mode 100644 index 000000000..8598d3300 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AICompletionRequest.cs @@ -0,0 +1,10 @@ +namespace Unity.GrantManager.AI +{ + public class AICompletionRequest + { + public string UserPrompt { get; set; } = string.Empty; + public string? SystemPrompt { get; set; } + public int MaxTokens { get; set; } = 150; + public double? Temperature { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs new file mode 100644 index 000000000..5ebbf4df9 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs @@ -0,0 +1,20 @@ +namespace Unity.GrantManager.AI +{ + public static class AIJsonKeys + { + public const string Rating = "rating"; + public const string Errors = "errors"; + public const string Warnings = "warnings"; + public const string Summaries = "summaries"; + public const string Dismissed = "dismissed"; + + public const string Id = "id"; + public const string Title = "title"; + public const string Detail = "detail"; + public const string Summary = "summary"; + + public const string Answer = "answer"; + public const string Rationale = "rationale"; + public const string Confidence = "confidence"; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisRequest.cs new file mode 100644 index 000000000..bf676f06a --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisRequest.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Text.Json; + +namespace Unity.GrantManager.AI +{ + public class ApplicationAnalysisRequest + { + public JsonElement Schema { get; set; } + public JsonElement Data { get; set; } + public List Attachments { get; set; } = new(); + public string? Rubric { get; set; } + } + + public class ApplicationAnalysisAttachment + { + public string Name { get; set; } = string.Empty; + public string Summary { get; set; } = string.Empty; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisResponse.cs new file mode 100644 index 000000000..f766a1f4d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ApplicationAnalysisResponse.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Unity.GrantManager.AI +{ + public class ApplicationAnalysisResponse + { + [JsonPropertyName(AIJsonKeys.Rating)] + public string? Rating { get; set; } + + [JsonPropertyName(AIJsonKeys.Errors)] + public List Errors { get; set; } = new(); + + [JsonPropertyName(AIJsonKeys.Warnings)] + public List Warnings { get; set; } = new(); + + [JsonPropertyName(AIJsonKeys.Summaries)] + public List Summaries { get; set; } = new(); + + [JsonPropertyName(AIJsonKeys.Dismissed)] + public List Dismissed { get; set; } = new(); + } + + public class ApplicationAnalysisFinding + { + [JsonPropertyName(AIJsonKeys.Id)] + public string? Id { get; set; } + + [JsonPropertyName(AIJsonKeys.Title)] + public string? Title { get; set; } + + [JsonPropertyName(AIJsonKeys.Detail)] + public string? Detail { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AttachmentSummaryRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AttachmentSummaryRequest.cs new file mode 100644 index 000000000..2cce56ae7 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AttachmentSummaryRequest.cs @@ -0,0 +1,9 @@ +namespace Unity.GrantManager.AI +{ + public class AttachmentSummaryRequest + { + public string FileName { get; set; } = string.Empty; + public byte[] FileContent { get; set; } = System.Array.Empty(); + public string ContentType { get; set; } = "application/octet-stream"; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs index e4c3d26ac..b8cb35632 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs @@ -6,6 +6,13 @@ namespace Unity.GrantManager.AI public interface IAIService { Task IsAvailableAsync(); + + Task GenerateCompletionAsync(AICompletionRequest request); + Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request); + Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request); + Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request); + + // Legacy compatibility methods retained until flow orchestration refactor. Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150); Task GenerateAttachmentSummaryAsync(string fileName, byte[] fileContent, string contentType); Task AnalyzeApplicationAsync(string applicationContent, List attachmentSummaries, string rubric, string? formFieldConfiguration = null); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ScoresheetSectionRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ScoresheetSectionRequest.cs new file mode 100644 index 000000000..f20d4935e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ScoresheetSectionRequest.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Text.Json; + +namespace Unity.GrantManager.AI +{ + public class ScoresheetSectionRequest + { + public JsonElement Data { get; set; } + public List Attachments { get; set; } = new(); + public string SectionName { get; set; } = string.Empty; + public JsonElement SectionSchema { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs index ad7786c4b..3547fc8dc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -54,6 +54,60 @@ public Task IsAvailableAsync() return Task.FromResult(true); } + public Task GenerateCompletionAsync(AICompletionRequest request) + { + return GenerateSummaryAsync( + request?.UserPrompt ?? string.Empty, + request?.SystemPrompt, + request?.MaxTokens ?? 150); + } + + public Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request) + { + return GenerateAttachmentSummaryAsync( + request?.FileName ?? string.Empty, + request?.FileContent ?? Array.Empty(), + request?.ContentType ?? "application/octet-stream"); + } + + public Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request) + { + var dataJson = JsonSerializer.Serialize(request.Data, new JsonSerializerOptions { WriteIndented = true }); + var schemaJson = JsonSerializer.Serialize(request.Schema, new JsonSerializerOptions { WriteIndented = true }); + + var attachmentSummaries = request.Attachments + .Select(a => $"{a.Name}: {a.Summary}") + .ToList(); + + var applicationContent = $@"DATA +{dataJson}"; + + var formFieldConfiguration = $@"SCHEMA +{schemaJson}"; + + return AnalyzeApplicationAsync( + applicationContent, + attachmentSummaries, + request.Rubric ?? string.Empty, + formFieldConfiguration); + } + + public Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request) + { + var dataJson = JsonSerializer.Serialize(request.Data, new JsonSerializerOptions { WriteIndented = true }); + var sectionJson = JsonSerializer.Serialize(request.SectionSchema, new JsonSerializerOptions { WriteIndented = true }); + + var attachmentSummaries = request.Attachments + .Select(a => $"{a.Name}: {a.Summary}") + .ToList(); + + return GenerateScoresheetSectionAnswersAsync( + dataJson, + attachmentSummaries, + sectionJson, + request.SectionName); + } + public async Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150) { if (string.IsNullOrEmpty(ApiKey)) From d6d0cf8d2d2dc24154706fe7f489504c7222e8c8 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 20 Feb 2026 17:56:10 -0800 Subject: [PATCH 2/3] Add typed AI analysis data to GrantApplicationDto --- .../GrantApplications/GrantApplicationDto.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs index 31a35a873..f2906d116 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Unity.GrantManager.AI; using Unity.GrantManager.ApplicationForms; using Volo.Abp.Application.Dtos; @@ -83,4 +84,5 @@ public class GrantApplicationDto : AuditedEntityDto public string? UnityApplicationId { get; set; } public string? ApplicantElectoralDistrict { get; set; } public string? AIAnalysis { get; set; } + public ApplicationAnalysisResponse? AIAnalysisData { get; set; } } From ceeb83fd9349f16ff8d083c16145deb514bb5982 Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Fri, 27 Feb 2026 17:00:22 -0800 Subject: [PATCH 3/3] AB#32010 Sonar fixes for overload adjacency and serializer options reuse --- .../AI/IAIService.cs | 4 +- .../AI/OpenAIService.cs | 52 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs index b8cb35632..ffddbbe6c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs @@ -9,14 +9,14 @@ public interface IAIService Task GenerateCompletionAsync(AICompletionRequest request); Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request); + Task GenerateAttachmentSummaryAsync(string fileName, byte[] fileContent, string contentType); Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request); Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request); + Task GenerateScoresheetSectionAnswersAsync(string applicationContent, List attachmentSummaries, string sectionJson, string sectionName); // Legacy compatibility methods retained until flow orchestration refactor. Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150); - Task GenerateAttachmentSummaryAsync(string fileName, byte[] fileContent, string contentType); Task AnalyzeApplicationAsync(string applicationContent, List attachmentSummaries, string rubric, string? formFieldConfiguration = null); Task GenerateScoresheetAnswersAsync(string applicationContent, List attachmentSummaries, string scoresheetQuestions); - Task GenerateScoresheetSectionAnswersAsync(string applicationContent, List attachmentSummaries, string sectionJson, string sectionName); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs index 3547fc8dc..3830cd75e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -62,18 +62,10 @@ public Task GenerateCompletionAsync(AICompletionRequest request) request?.MaxTokens ?? 150); } - public Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request) - { - return GenerateAttachmentSummaryAsync( - request?.FileName ?? string.Empty, - request?.FileContent ?? Array.Empty(), - request?.ContentType ?? "application/octet-stream"); - } - public Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request) { - var dataJson = JsonSerializer.Serialize(request.Data, new JsonSerializerOptions { WriteIndented = true }); - var schemaJson = JsonSerializer.Serialize(request.Schema, new JsonSerializerOptions { WriteIndented = true }); + var dataJson = JsonSerializer.Serialize(request.Data, JsonLogOptions); + var schemaJson = JsonSerializer.Serialize(request.Schema, JsonLogOptions); var attachmentSummaries = request.Attachments .Select(a => $"{a.Name}: {a.Summary}") @@ -92,22 +84,6 @@ public Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest formFieldConfiguration); } - public Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request) - { - var dataJson = JsonSerializer.Serialize(request.Data, new JsonSerializerOptions { WriteIndented = true }); - var sectionJson = JsonSerializer.Serialize(request.SectionSchema, new JsonSerializerOptions { WriteIndented = true }); - - var attachmentSummaries = request.Attachments - .Select(a => $"{a.Name}: {a.Summary}") - .ToList(); - - return GenerateScoresheetSectionAnswersAsync( - dataJson, - attachmentSummaries, - sectionJson, - request.SectionName); - } - public async Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150) { if (string.IsNullOrEmpty(ApiKey)) @@ -231,6 +207,14 @@ Produce a concise reviewer-facing summary of the provided attachment context. } } + public Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request) + { + return GenerateAttachmentSummaryAsync( + request?.FileName ?? string.Empty, + request?.FileContent ?? Array.Empty(), + request?.ContentType ?? "application/octet-stream"); + } + public async Task AnalyzeApplicationAsync(string applicationContent, List attachmentSummaries, string rubric, string? formFieldConfiguration = null) { if (string.IsNullOrEmpty(ApiKey)) @@ -566,6 +550,22 @@ public async Task GenerateScoresheetSectionAnswersAsync(string applicati } } + public Task GenerateScoresheetSectionAnswersAsync(ScoresheetSectionRequest request) + { + var dataJson = JsonSerializer.Serialize(request.Data, JsonLogOptions); + var sectionJson = JsonSerializer.Serialize(request.SectionSchema, JsonLogOptions); + + var attachmentSummaries = request.Attachments + .Select(a => $"{a.Name}: {a.Summary}") + .ToList(); + + return GenerateScoresheetSectionAnswersAsync( + dataJson, + attachmentSummaries, + sectionJson, + request.SectionName); + } + private async Task LogPromptInputAsync(string promptType, string? systemPrompt, string userPrompt) { var formattedInput = FormatPromptInputForLog(systemPrompt, userPrompt);