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; }
}