From 5638920bb07c13f927d8e95f03e02ba9ea2a0250 Mon Sep 17 00:00:00 2001 From: Sam Saravillo <7529759+samsaravillo@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:45:04 -0800 Subject: [PATCH 1/4] feature/AB#30835 Switch assessment generation to use Agentic AI - Replaced OpenAI summary generation with a new method that calls the Agentic AI API using a resilient HTTP request. - Updated relevant methods to use GenerateAgenticSummaryAsync for improved reliability and consistency. --- .../AI/OpenAIService.cs | 79 +++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) 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 9fc791051..cb7dc7330 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -7,6 +7,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Unity.Modules.Shared.Http; using Volo.Abp.DependencyInjection; namespace Unity.GrantManager.AI @@ -17,17 +18,21 @@ public class OpenAIService : IAIService, ITransientDependency private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly ITextExtractionService _textExtractionService; + private readonly IResilientHttpRequest _resilientHttpRequest; private string? ApiKey => _configuration["AI:OpenAI:ApiKey"]; private string? ApiUrl => _configuration["AI:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; + private string? AgenticApiUrl => _configuration["AI:AgenticAPI:Url"]; private readonly string NoKeyError = "OpenAI API key is not configured"; + - public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService) + public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService, IResilientHttpRequest resilientHttpRequest) { _httpClient = httpClient; _configuration = configuration; _logger = logger; _textExtractionService = textExtractionService; + _resilientHttpRequest = resilientHttpRequest; } public Task IsAvailableAsync() @@ -100,6 +105,70 @@ public async Task GenerateSummaryAsync(string content, string? prompt = } } + private async Task GenerateAgenticSummaryAsync( + string userPrompt, + string systemPrompt, + int maxTokens = 150) + { + + int numSelfConsistency = 3; + int numCot = 1; + string model = "fast"; + double temperature = 0.3; + + try + { + var requestBody = new + { + prompt = userPrompt, + system_prompt = systemPrompt ?? "You are a professional grant analyst for the BC Government.", + num_self_consistency = numSelfConsistency, + num_cot = numCot, + model, + temperature, + max_tokens = maxTokens + }; + + var response = await _resilientHttpRequest.HttpAsync( + HttpMethod.Post, + AgenticApiUrl!, + requestBody + ); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogError("Agentic AI API request failed: {StatusCode} - {Content}", + response.StatusCode, errorContent); + return "AI analysis failed - service temporarily unavailable."; + } + + var responseContent = await response.Content.ReadAsStringAsync(); + _logger.LogDebug("Response: {Response}", responseContent); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("OpenAI API request failed: {StatusCode} - {Content}", response.StatusCode, responseContent); + return "AI analysis failed - service temporarily unavailable."; + } + + using var jsonDoc = JsonDocument.Parse(responseContent); + var choices = jsonDoc.RootElement.GetProperty("choices"); + if (choices.GetArrayLength() > 0) + { + var message = choices[0].GetProperty("message"); + return message.GetProperty("content").GetString() ?? "No summary generated."; + } + + return "No summary generated."; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling Agentic AI API"); + return "AI analysis failed - please try again later."; + } + } + public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] fileContent, string contentType) { try @@ -124,7 +193,7 @@ public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] prompt = "Please analyze this document and provide a concise summary of its content, purpose, and key information, for use by your fellow grant analysts. It should be 1-2 sentences long and about 46 tokens."; } - return await GenerateSummaryAsync(contentToAnalyze, prompt, 150); + return await GenerateAgenticSummaryAsync(contentToAnalyze, prompt, 150); } catch (Exception ex) { @@ -183,7 +252,7 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis Be thorough but fair in your assessment. Focus on compliance, completeness, and alignment with program requirements. Respond only with valid JSON in the exact format requested."; - return await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); + return await GenerateAgenticSummaryAsync(analysisContent, systemPrompt, 1000); } catch (Exception ex) { @@ -237,7 +306,7 @@ Base your answers on the application content and attachment summaries provided. Be thorough, objective, and fair in your assessment. Base your answers strictly on the provided application content. Respond only with valid JSON in the exact format requested."; - return await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); + return await GenerateAgenticSummaryAsync(analysisContent, systemPrompt, 2000); } catch (Exception ex) { @@ -311,7 +380,7 @@ Always provide citations that reference specific parts of the application conten Be honest about your confidence level - if information is missing or unclear, reflect this in a lower confidence score. Respond only with valid JSON in the exact format requested."; - return await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); + return await GenerateAgenticSummaryAsync(analysisContent, systemPrompt, 2000); } catch (Exception ex) { From 63bad4c7199017b66af4a50b3e5c6b81d426a293 Mon Sep 17 00:00:00 2001 From: Sam Saravillo <7529759+samsaravillo@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:41:35 -0800 Subject: [PATCH 2/4] feature/AB#30835 SQ fix -sonarqube fix --- .../AI/OpenAIService.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 cb7dc7330..7c2b9835c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -24,7 +24,9 @@ public class OpenAIService : IAIService, ITransientDependency private string? ApiUrl => _configuration["AI:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; private string? AgenticApiUrl => _configuration["AI:AgenticAPI:Url"]; private readonly string NoKeyError = "OpenAI API key is not configured"; - + private readonly string NoSummaryMsg = "No summary generated."; + + public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService, IResilientHttpRequest resilientHttpRequest) { @@ -93,10 +95,10 @@ public async Task GenerateSummaryAsync(string content, string? prompt = if (choices.GetArrayLength() > 0) { var message = choices[0].GetProperty("message"); - return message.GetProperty("content").GetString() ?? "No summary generated."; + return message.GetProperty("content").GetString() ?? NoSummaryMsg; } - return "No summary generated."; + return NoSummaryMsg; } catch (Exception ex) { @@ -157,10 +159,10 @@ private async Task GenerateAgenticSummaryAsync( if (choices.GetArrayLength() > 0) { var message = choices[0].GetProperty("message"); - return message.GetProperty("content").GetString() ?? "No summary generated."; + return message.GetProperty("content").GetString() ?? NoSummaryMsg; } - return "No summary generated."; + return NoSummaryMsg; } catch (Exception ex) { From a859030f7c7212e1ba0e2d348cc7f2d66a0fdb8b Mon Sep 17 00:00:00 2001 From: Sam Saravillo <7529759+samsaravillo@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:31:48 -0800 Subject: [PATCH 3/4] feature/AB#30836 add ai agent endpoint and update GenerateAgenticSummaryAsync - Added AGENTIC_AI key to DynamicUrlKeyNames and seeded its default value in DynamicUrlDataSeeder. - Updated OpenAIService to retrieve the Agentic AI endpoint dynamically via IEndpointManagementAppService and refactored related method calls to use the standard summary generation method. --- .../AI/OpenAIService.cs | 32 +++++++++++-------- .../Integrations/DynamicUrlKeyNames.cs | 1 + .../Integrations/DynamicUrlDataSeeder.cs | 2 ++ 3 files changed, 22 insertions(+), 13 deletions(-) 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 7c2b9835c..072fd97cb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -7,6 +7,8 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Unity.GrantManager.Integrations; +using Unity.GrantManager.Integrations.Endpoints; using Unity.Modules.Shared.Http; using Volo.Abp.DependencyInjection; @@ -19,22 +21,21 @@ public class OpenAIService : IAIService, ITransientDependency private readonly ILogger _logger; private readonly ITextExtractionService _textExtractionService; private readonly IResilientHttpRequest _resilientHttpRequest; + private readonly IEndpointManagementAppService _endpointManagementAppService; private string? ApiKey => _configuration["AI:OpenAI:ApiKey"]; private string? ApiUrl => _configuration["AI:OpenAI:ApiUrl"] ?? "https://api.openai.com/v1/chat/completions"; - private string? AgenticApiUrl => _configuration["AI:AgenticAPI:Url"]; private readonly string NoKeyError = "OpenAI API key is not configured"; private readonly string NoSummaryMsg = "No summary generated."; - - - public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService, IResilientHttpRequest resilientHttpRequest) + public OpenAIService(HttpClient httpClient, IConfiguration configuration, ILogger logger, ITextExtractionService textExtractionService, IResilientHttpRequest resilientHttpRequest, IEndpointManagementAppService endpointManagementAppService) { _httpClient = httpClient; _configuration = configuration; _logger = logger; _textExtractionService = textExtractionService; _resilientHttpRequest = resilientHttpRequest; + _endpointManagementAppService = endpointManagementAppService; } public Task IsAvailableAsync() @@ -48,6 +49,7 @@ public Task IsAvailableAsync() return Task.FromResult(true); } + //Unity AI method public async Task GenerateSummaryAsync(string content, string? prompt = null, int maxTokens = 150) { if (string.IsNullOrEmpty(ApiKey)) @@ -95,6 +97,7 @@ public async Task GenerateSummaryAsync(string content, string? prompt = if (choices.GetArrayLength() > 0) { var message = choices[0].GetProperty("message"); + var test = message.GetProperty("content").GetString(); return message.GetProperty("content").GetString() ?? NoSummaryMsg; } @@ -107,6 +110,7 @@ public async Task GenerateSummaryAsync(string content, string? prompt = } } + //AI agent method TODO/TBD: Once the AI agent code has hosted domain we can able to use this and update the endpoint management private async Task GenerateAgenticSummaryAsync( string userPrompt, string systemPrompt, @@ -118,6 +122,8 @@ private async Task GenerateAgenticSummaryAsync( string model = "fast"; double temperature = 0.3; + var aiAgentBaseUri = await _endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.AGENTIC_AI); + try { var requestBody = new @@ -133,7 +139,7 @@ private async Task GenerateAgenticSummaryAsync( var response = await _resilientHttpRequest.HttpAsync( HttpMethod.Post, - AgenticApiUrl!, + aiAgentBaseUri!, requestBody ); @@ -155,11 +161,11 @@ private async Task GenerateAgenticSummaryAsync( } using var jsonDoc = JsonDocument.Parse(responseContent); - var choices = jsonDoc.RootElement.GetProperty("choices"); - if (choices.GetArrayLength() > 0) + var msg = jsonDoc.RootElement.GetProperty("final_answer"); + + if (jsonDoc.RootElement.TryGetProperty("final_answer", out var finalAnswer) && msg.ValueKind != JsonValueKind.Null) { - var message = choices[0].GetProperty("message"); - return message.GetProperty("content").GetString() ?? NoSummaryMsg; + return msg.GetString() ?? NoSummaryMsg; } return NoSummaryMsg; @@ -195,7 +201,7 @@ public async Task GenerateAttachmentSummaryAsync(string fileName, byte[] prompt = "Please analyze this document and provide a concise summary of its content, purpose, and key information, for use by your fellow grant analysts. It should be 1-2 sentences long and about 46 tokens."; } - return await GenerateAgenticSummaryAsync(contentToAnalyze, prompt, 150); + return await GenerateSummaryAsync(contentToAnalyze, prompt, 150); } catch (Exception ex) { @@ -254,7 +260,7 @@ public async Task AnalyzeApplicationAsync(string applicationContent, Lis Be thorough but fair in your assessment. Focus on compliance, completeness, and alignment with program requirements. Respond only with valid JSON in the exact format requested."; - return await GenerateAgenticSummaryAsync(analysisContent, systemPrompt, 1000); + return await GenerateSummaryAsync(analysisContent, systemPrompt, 1000); } catch (Exception ex) { @@ -308,7 +314,7 @@ Base your answers on the application content and attachment summaries provided. Be thorough, objective, and fair in your assessment. Base your answers strictly on the provided application content. Respond only with valid JSON in the exact format requested."; - return await GenerateAgenticSummaryAsync(analysisContent, systemPrompt, 2000); + return await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); } catch (Exception ex) { @@ -382,7 +388,7 @@ Always provide citations that reference specific parts of the application conten Be honest about your confidence level - if information is missing or unclear, reflect this in a lower confidence score. Respond only with valid JSON in the exact format requested."; - return await GenerateAgenticSummaryAsync(analysisContent, systemPrompt, 2000); + return await GenerateSummaryAsync(analysisContent, systemPrompt, 2000); } catch (Exception ex) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs index f6f2147a9..46b68685e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Integrations/DynamicUrlKeyNames.cs @@ -14,4 +14,5 @@ public static class DynamicUrlKeyNames public const string WEBHOOK_KEY_PREFIX = "WEBHOOK_"; // General Webhook URL - Dynamically incremented public const string GEOCODER_API_BASE = "GEOCODER_API_BASE"; public const string GEOCODER_LOCATION_API_BASE = "GEOCODER_LOCATION_API_BASE"; + public const string AGENTIC_AI = "AGENTIC_AI"; } \ No newline at end of file diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs index ea707ca86..bfa102f82 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Integrations/DynamicUrlDataSeeder.cs @@ -31,6 +31,7 @@ public static class DynamicUrls public const string GEOCODER_BASE_URL = $"{PROTOCOL}//openmaps.gov.bc.ca/geo/pub/ows?service=WFS&version=1.0.0&request=GetFeature&typeName="; public const string GEOCODER_LOCATION_BASE_URL = $"{PROTOCOL}//geocoder.api.gov.bc.ca"; public const string REPORTING_AI = $"{PROTOCOL}//reporting.grants.gov.bc.ca"; + public const string AGENTIC_AI = $"{PROTOCOL}//localhost:5000/v1/completions"; } private async Task SeedDynamicUrlAsync() @@ -57,6 +58,7 @@ private async Task SeedDynamicUrlAsync() new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, new() { KeyName = $"{DynamicUrlKeyNames.WEBHOOK_KEY_PREFIX}{webhookIndex++}", Url = "", Description = $"Webhook {webhookIndex}" }, + new() { KeyName = DynamicUrlKeyNames.AGENTIC_AI, Url = DynamicUrls.AGENTIC_AI, Description = "Agentic AI Source" }, }; foreach (var dynamicUrl in dynamicUrls) From 3a094b28e68141403b2b15ac093ac27f5eeabab3 Mon Sep 17 00:00:00 2001 From: Sam Saravillo <7529759+samsaravillo@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:36:20 -0800 Subject: [PATCH 4/4] SQ Fix sq fix --- .../src/Unity.GrantManager.Application/AI/OpenAIService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 072fd97cb..06d0ef5ce 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/OpenAIService.cs @@ -8,7 +8,6 @@ using System.Text.Json; using System.Threading.Tasks; using Unity.GrantManager.Integrations; -using Unity.GrantManager.Integrations.Endpoints; using Unity.Modules.Shared.Http; using Volo.Abp.DependencyInjection; @@ -97,7 +96,6 @@ public async Task GenerateSummaryAsync(string content, string? prompt = if (choices.GetArrayLength() > 0) { var message = choices[0].GetProperty("message"); - var test = message.GetProperty("content").GetString(); return message.GetProperty("content").GetString() ?? NoSummaryMsg; } @@ -110,7 +108,7 @@ public async Task GenerateSummaryAsync(string content, string? prompt = } } - //AI agent method TODO/TBD: Once the AI agent code has hosted domain we can able to use this and update the endpoint management + //AI agent method: Once the AI agent code has hosted domain we can able to use this and update the endpoint management private async Task GenerateAgenticSummaryAsync( string userPrompt, string systemPrompt,