using EAIAdminBLL.Sync; using EAIAdminDAL.CustomCode; using EAIAdminDAL.DTO; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace EAIAdminBLL.EAIExecution { public class EAIExecutionBLL : IEAIExecutionBLL { private const string AgentSlug = "report_analyzer"; private readonly IEAIExecutionSyncClient _syncClient; private readonly IEAIReportLookupDAL _reportLookupDAL; private readonly IEAIAgentRouteAdminDAL _agentRouteDAL; private readonly ILogger _logger; public EAIExecutionBLL( IEAIExecutionSyncClient syncClient, IEAIReportLookupDAL reportLookupDAL, IEAIAgentRouteAdminDAL agentRouteDAL, ILogger logger) { _syncClient = syncClient ?? throw new ArgumentNullException(nameof(syncClient)); _reportLookupDAL = reportLookupDAL ?? throw new ArgumentNullException(nameof(reportLookupDAL)); _agentRouteDAL = agentRouteDAL ?? throw new ArgumentNullException(nameof(agentRouteDAL)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task AnalyzeReportAsync( EAIAnalyzeReportRequestDTO dto, LoginDTO login, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(dto.RowData)) throw new ArgumentException("RowData is required."); try { // The Angular report catalog doesn't surface ReportCode today (only ReportId) — // resolve it server-side so each report type still gets its own prompt variant // even when the caller only had ReportId to send. var reportCode = dto.ReportCode; if (string.IsNullOrWhiteSpace(reportCode) && dto.ReportId > 0) reportCode = await _reportLookupDAL.GetReportCodeAsync(dto.ReportId, login, ct) .ConfigureAwait(false); // {"message": [...]} — matches the downstream analyzer's existing contract // (gbaicoanalyst.service.ts::loadAnalystData posts {message: rowData} today). // No session_id form field here — gbEAI's agent_catalog picks ANALYZE_REPORT vs // QUESTION_ANSWER purely on session_id presence; the session is minted by the // downstream analyzer itself and returned in the response. using var rowsDoc = JsonDocument.Parse(dto.RowData); var inputJson = JsonSerializer.Serialize(new Dictionary { ["message"] = rowsDoc.RootElement.Clone() }); var formFields = new Dictionary { ["input"] = inputJson, ["variant_code"] = reportCode ?? string.Empty }; var response = await _syncClient .ExecuteAgentAsync(AgentSlug, formFields, login, ct: ct) .ConfigureAwait(false); return ParseResult(response, isChat: false); } catch (Exception ex) { _logger.LogError(ex, "BLL | AnalyzeReport | Error | Tenant={TenantId} ReportId={ReportId} ReportCode={ReportCode}", login.ClientId, dto.ReportId, dto.ReportCode); throw; } } public async Task AnalyzeReportChatAsync( EAIAnalyzeReportChatRequestDTO dto, LoginDTO login, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(dto.SessionId)) throw new ArgumentException("SessionId is required."); if (string.IsNullOrWhiteSpace(dto.Message)) throw new ArgumentException("Message is required."); try { // Plain text "input" + a "session_id" form field — presence of session_id is what // makes gbEAI's agent_catalog pick QUESTION_ANSWER over ANALYZE_REPORT. var formFields = new Dictionary { ["input"] = dto.Message, ["session_id"] = dto.SessionId }; var response = await _syncClient .ExecuteAgentAsync(AgentSlug, formFields, login, ct: ct) .ConfigureAwait(false); return ParseResult(response, isChat: true); } catch (Exception ex) { _logger.LogError(ex, "BLL | AnalyzeReportChat | Error | Tenant={TenantId} SessionId={SessionId}", login.ClientId, dto.SessionId); throw; } } public async Task ExecuteCapabilityAsync( EAIExecuteCapabilityRequestDTO dto, LoginDTO login, (string FileName, byte[] Bytes)? file = null, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(dto.InitiativeCode)) throw new ArgumentException("InitiativeCode is required."); if (string.IsNullOrWhiteSpace(dto.CapabilityCode)) throw new ArgumentException("CapabilityCode is required."); if (string.IsNullOrWhiteSpace(dto.InputJson) && file is null) throw new ArgumentException("Either InputJson or a file is required."); try { var slug = await _agentRouteDAL .ResolveSlugForCapabilityAsync(dto.InitiativeCode, dto.CapabilityCode, login, ct) .ConfigureAwait(false); if (string.IsNullOrWhiteSpace(slug)) throw new InvalidOperationException( $"No active EAI.MEAIAGENTROUTE serves capability '{dto.CapabilityCode}' " + $"under initiative '{dto.InitiativeCode}'."); var formFields = new Dictionary(); if (!string.IsNullOrWhiteSpace(dto.InputJson)) formFields["input"] = dto.InputJson; if (!string.IsNullOrWhiteSpace(dto.VariantCode)) formFields["variant_code"] = dto.VariantCode; var response = await _syncClient .ExecuteAgentAsync(slug, formFields, login, file, ct) .ConfigureAwait(false); if (!response.IsSuccess) throw new InvalidOperationException( $"AI-Enterprise-v1.0 call failed: {response.ErrorMessage}"); using var doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(response.Body) ? "{}" : response.Body); var resultJson = doc.RootElement.TryGetProperty("result", out var result) ? result.GetRawText() : "{}"; return new EAIExecuteCapabilityResultDTO { ResultJson = resultJson }; } catch (Exception ex) { _logger.LogError(ex, "BLL | ExecuteCapability | Error | Tenant={TenantId} Initiative={InitiativeCode} Capability={CapabilityCode}", login.ClientId, dto.InitiativeCode, dto.CapabilityCode); throw; } } // gbEAI's ExecuteResponse shape is {executionId, result}; `result` is whatever the // downstream analyzer returned verbatim — {session_id, summary, chart} for // /upload-json, {response|messages} for /chat (confirmed from // gbaicoanalyst.component.ts's existing dev-mode response handling). private static EAIAnalyzeReportResultDTO ParseResult( GB5Shared.ExternalAI.AIEngineResponse response, bool isChat) { if (!response.IsSuccess) throw new InvalidOperationException( $"AI-Enterprise-v1.0 call failed: {response.ErrorMessage}"); var dto = new EAIAnalyzeReportResultDTO(); using var doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(response.Body) ? "{}" : response.Body); if (!doc.RootElement.TryGetProperty("result", out var result)) return dto; if (result.TryGetProperty("session_id", out var sid) && sid.ValueKind == JsonValueKind.String) dto.SessionId = sid.GetString(); if (!isChat) { if (result.TryGetProperty("summary", out var summary) && summary.ValueKind == JsonValueKind.String) dto.Analysis = summary.GetString() ?? string.Empty; if (result.TryGetProperty("chart", out var chart) && chart.ValueKind == JsonValueKind.String) dto.ChartBase64 = chart.GetString(); } else { if (result.TryGetProperty("response", out var resp) && resp.ValueKind == JsonValueKind.String) dto.Analysis = resp.GetString() ?? string.Empty; else if (result.TryGetProperty("messages", out var msgs) && msgs.ValueKind == JsonValueKind.String) dto.Analysis = msgs.GetString() ?? string.Empty; } return dto; } } }