using Flurl.Http; using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.DataTypes; using Newtonsoft.Json; namespace InnovEnergy.App.Backend.Services; /// /// Generates AI-powered diagnoses for support tickets. /// Runs async after ticket creation; stores result in TicketAiDiagnosis table. /// public static class TicketDiagnosticService { private static string _apiKey = ""; private static readonly string MistralUrl = "https://api.mistral.ai/v1/chat/completions"; public static void Initialize() { var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY"); if (string.IsNullOrWhiteSpace(apiKey)) Console.Error.WriteLine("[TicketDiagnosticService] MISTRAL_API_KEY not set – ticket AI disabled."); else _apiKey = apiKey; Console.WriteLine("[TicketDiagnosticService] initialised."); } public static bool IsEnabled => !string.IsNullOrEmpty(_apiKey); /// /// Called fire-and-forget after ticket creation. /// Creates a TicketAiDiagnosis row (Pending → Analyzing → Completed | Failed). /// public static async Task DiagnoseTicketAsync(Int64 ticketId) { var ticket = Db.GetTicketById(ticketId); if (ticket is null) return; var installation = Db.GetInstallationById(ticket.InstallationId); if (installation is null) return; var diagnosis = new TicketAiDiagnosis { TicketId = ticketId, Status = (Int32)DiagnosisStatus.Pending, CreatedAt = DateTime.UtcNow }; Db.Create(diagnosis); if (!IsEnabled) { diagnosis.Status = (Int32)DiagnosisStatus.Failed; Db.Update(diagnosis); return; } diagnosis.Status = (Int32)DiagnosisStatus.Analyzing; Db.Update(diagnosis); try { var productName = ((ProductType)installation.Product).ToString(); var recentErrors = Db.Errors .Where(e => e.InstallationId == ticket.InstallationId) .OrderByDescending(e => e.Date) .ToList() .Select(e => e.Description) .Distinct() .Take(5) .ToList(); var prompt = BuildPrompt(ticket, productName, recentErrors); var result = await CallMistralAsync(prompt); if (result is null) { diagnosis.Status = (Int32)DiagnosisStatus.Failed; } else { diagnosis.Status = (Int32)DiagnosisStatus.Completed; diagnosis.RootCause = result.RootCause; diagnosis.Confidence = result.Confidence; diagnosis.RecommendedActions = result.RecommendedActionsJson; diagnosis.CompletedAt = DateTime.UtcNow; } } catch (Exception ex) { Console.Error.WriteLine($"[TicketDiagnosticService] {ex.Message}"); diagnosis.Status = (Int32)DiagnosisStatus.Failed; } Db.Update(diagnosis); Db.Create(new TicketTimelineEvent { TicketId = ticketId, EventType = (Int32)TimelineEventType.AiDiagnosisAttached, Description = diagnosis.Status == (Int32)DiagnosisStatus.Completed ? "AI diagnosis completed." : "AI diagnosis failed.", ActorType = (Int32)TimelineActorType.AiAgent, CreatedAt = DateTime.UtcNow }); } private static string BuildPrompt(Ticket ticket, string productName, List recentErrors) { var recentList = recentErrors.Count > 0 ? string.Join(", ", recentErrors) : "none"; return $@"You are a senior field technician for {productName} battery energy storage systems. A support ticket has been submitted with the following details: Subject: {ticket.Subject} Description: {ticket.Description} Category: {(TicketCategory)ticket.Category} Priority: {(TicketPriority)ticket.Priority} Recent system alarms: {recentList} Analyze this ticket and respond in JSON only — no markdown, no explanation outside JSON: {{ ""rootCause"": ""One concise sentence describing the most likely root cause."", ""confidence"": 0.85, ""recommendedActions"": [""Action 1"", ""Action 2"", ""Action 3""] }} Confidence must be a number between 0.0 and 1.0."; } private static async Task CallMistralAsync(string prompt) { try { var body = new { model = "mistral-small-latest", messages = new[] { new { role = "user", content = prompt } }, max_tokens = 300, temperature = 0.2 }; var text = await MistralUrl .WithHeader("Authorization", $"Bearer {_apiKey}") .PostJsonAsync(body) .ReceiveString(); var envelope = JsonConvert.DeserializeObject(text); var content = (string?)envelope?.choices?[0]?.message?.content; if (string.IsNullOrWhiteSpace(content)) return null; var json = content.Trim(); if (json.StartsWith("```")) { var nl = json.IndexOf('\n'); if (nl >= 0) json = json[(nl + 1)..]; if (json.EndsWith("```")) json = json[..^3]; json = json.Trim(); } var parsed = JsonConvert.DeserializeObject(json); if (parsed is null) return null; return new TicketDiagnosisResult { RootCause = parsed.RootCause, Confidence = parsed.Confidence, RecommendedActionsJson = JsonConvert.SerializeObject(parsed.RecommendedActions ?? Array.Empty()) }; } catch (Exception ex) { Console.Error.WriteLine($"[TicketDiagnosticService] HTTP error: {ex.Message}"); return null; } } } internal class TicketDiagnosisRaw { [JsonProperty("rootCause")] public String? RootCause { get; set; } [JsonProperty("confidence")] public Double? Confidence { get; set; } [JsonProperty("recommendedActions")] public String[]? RecommendedActions { get; set; } } internal class TicketDiagnosisResult { public String? RootCause { get; set; } public Double? Confidence { get; set; } public String? RecommendedActionsJson { get; set; } }