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..06d0ef5ce 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.Modules.Shared.Http; using Volo.Abp.DependencyInjection; namespace Unity.GrantManager.AI @@ -17,17 +19,22 @@ public class OpenAIService : IAIService, ITransientDependency private readonly IConfiguration _configuration; 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 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) + 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() @@ -41,6 +48,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)) @@ -88,10 +96,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) { @@ -100,6 +108,73 @@ public async Task GenerateSummaryAsync(string content, string? prompt = } } + //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, + int maxTokens = 150) + { + + int numSelfConsistency = 3; + int numCot = 1; + string model = "fast"; + double temperature = 0.3; + + var aiAgentBaseUri = await _endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.AGENTIC_AI); + + 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, + aiAgentBaseUri!, + 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 msg = jsonDoc.RootElement.GetProperty("final_answer"); + + if (jsonDoc.RootElement.TryGetProperty("final_answer", out var finalAnswer) && msg.ValueKind != JsonValueKind.Null) + { + return msg.GetString() ?? NoSummaryMsg; + } + + return NoSummaryMsg; + } + 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 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)