using Azure.Core; using Azure.Identity; using Kusto.Data; using Kusto.Data.Common; using Kusto.Data.Net.Client; using Microsoft.Extensions.Options; using System.Text.Json; using Tango.Portal.Chat.Web.Models; namespace Tango.Portal.Chat.Web.Services { public sealed class ChatMessageLogger { private readonly ICslAdminProvider _admin; private readonly string _database; private readonly ILogger _logger; public ChatMessageLogger(IOptions opts, ILogger logger) { var options = opts.Value; _database = options.Database; _logger = logger; var cred = new ClientSecretCredential( options.TenantId, options.ClientId, options.ClientSecret); var kcsb = new KustoConnectionStringBuilder(options.ClusterUri) .WithAadAzureTokenCredentialsAuthentication(cred); _admin = KustoClientFactory.CreateCslAdminProvider(kcsb); } public async Task LogMessageAsync(ChatConversationMessage message, CancellationToken ct = default) { try { var json = JsonSerializer.Serialize(message); var command = $".ingest inline into table ChatConversationsTable with (jsonMappingReference = 'ChatConversationsTableMapping') <| {json}"; var props = new ClientRequestProperties { ClientRequestId = $"chat_log_{Guid.NewGuid()}" }; using var result = await _admin.ExecuteControlCommandAsync(_database, command, props); _logger.LogDebug("Successfully logged chat message {MessageId} for session {SessionId}", message.ID, message.SessionID); } catch (Exception ex) { _logger.LogError(ex, "Failed to log chat message {MessageId} for session {SessionId}", message.ID, message.SessionID); } } public async Task LogQuestionAsync(string sessionId, string userEmail, string userName, string question, CancellationToken ct = default) { var message = new ChatConversationMessage { SessionID = sessionId, Time = DateTime.UtcNow, Email = userEmail, User = userName, Role = "user", Classification = "question", Message = question, Provider = string.Empty, Assumptions = string.Empty }; await LogMessageAsync(message, ct); } public async Task LogAnswerAsync(string sessionId, string userEmail, string userName, ChatResponse response, string assistantType, string provider, List? assumptions, CancellationToken ct = default) { var answerMessage = BuildAnswerMessage(response); var message = new ChatConversationMessage { SessionID = sessionId, Time = DateTime.UtcNow, Email = userEmail, User = userName, Role = "assistant", Classification = assistantType, Message = answerMessage, Provider = provider, Assumptions = assumptions != null && assumptions.Count > 0 ? string.Join("; ", assumptions) : string.Empty }; await LogMessageAsync(message, ct); } private static string BuildAnswerMessage(ChatResponse response) { var parts = new List(); if (!string.IsNullOrEmpty(response.Answer)) { parts.Add($"Answer: {response.Answer}"); } if (!string.IsNullOrEmpty(response.UsedKql)) { parts.Add($"Used KQL: {response.UsedKql}"); } if (!string.IsNullOrEmpty(response.Ploty)) { parts.Add($"Visualization: {response.Ploty}"); } return parts.Count > 0 ? string.Join("\n\n", parts) : response.Answer ?? string.Empty; } } }