diff --git a/.gitignore b/.gitignore index 7a1e5ac79..1a9d879a1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ **/obj *.DotSettings.user **/.idea/ - +**/.env diff --git a/csharp/App/Backend/Backend.csproj b/csharp/App/Backend/Backend.csproj index 518c98e37..3021dc3a3 100644 --- a/csharp/App/Backend/Backend.csproj +++ b/csharp/App/Backend/Backend.csproj @@ -43,6 +43,9 @@ PreserveNewest + + PreserveNewest + diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 86c87633d..bf0fb6383 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -741,7 +741,7 @@ public class Controller : ControllerBase /// /// Returns an AI-generated diagnosis for a single error/alarm description. /// Responses are cached in memory — repeated calls for the same error code - /// do not hit OpenAI again. + /// do not hit Mistral again. /// [HttpGet(nameof(DiagnoseError))] public async Task> DiagnoseError(Int64 installationId, string errorDescription, Token authToken) @@ -791,7 +791,7 @@ public class Controller : ControllerBase "Warning 500", "Error 408", "AFCI Fault", - // Unknown alarm (should return null - would call OpenAI) + // Unknown alarm (should return null - would call Mistral) "Some unknown alarm XYZ123" }; @@ -803,7 +803,7 @@ public class Controller : ControllerBase { Alarm = alarm, FoundInKnowledgeBase = diagnosis != null, - Explanation = diagnosis?.Explanation ?? "NOT FOUND - Would call OpenAI API", + Explanation = diagnosis?.Explanation ?? "NOT FOUND - Would call Mistral API", CausesCount = diagnosis?.Causes.Count ?? 0, NextStepsCount = diagnosis?.NextSteps.Count ?? 0 }); @@ -814,7 +814,7 @@ public class Controller : ControllerBase TestTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), TotalTests = testCases.Length, FoundInKnowledgeBase = results.Count(r => ((dynamic)r).FoundInKnowledgeBase), - WouldCallOpenAI = results.Count(r => !((dynamic)r).FoundInKnowledgeBase), + WouldCallMistral = results.Count(r => !((dynamic)r).FoundInKnowledgeBase), Results = results }); } diff --git a/csharp/App/Backend/Program.cs b/csharp/App/Backend/Program.cs index 56208e0f1..8bc1da94c 100644 --- a/csharp/App/Backend/Program.cs +++ b/csharp/App/Backend/Program.cs @@ -25,6 +25,7 @@ public static class Program Watchdog.NotifyReady(); Db.Init(); + LoadEnvFile(); DiagnosticService.Initialize(); var builder = WebApplication.CreateBuilder(args); @@ -89,6 +90,33 @@ public static class Program app.Run(); } + private static void LoadEnvFile() + { + var envPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".env"); + + if (!File.Exists(envPath)) + envPath = ".env"; // fallback for dev + + if (!File.Exists(envPath)) + return; + + foreach (var line in File.ReadAllLines(envPath)) + { + var trimmed = line.Trim(); + if (trimmed.Length == 0 || trimmed.StartsWith('#')) + continue; + + var idx = trimmed.IndexOf('='); + if (idx <= 0) + continue; + + var key = trimmed[..idx].Trim(); + var value = trimmed[(idx + 1)..].Trim(); + + Environment.SetEnvironmentVariable(key, value); + } + } + private static OpenApiInfo OpenApiInfo { get; } = new OpenApiInfo { Title = "Inesco Backend API", diff --git a/csharp/App/Backend/Resources/openAiConfig.json b/csharp/App/Backend/Resources/openAiConfig.json deleted file mode 100644 index 43338afc4..000000000 --- a/csharp/App/Backend/Resources/openAiConfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ApiKey": "sk-your-openai-api-key-here" -} diff --git a/csharp/App/Backend/Services/AlarmKnowledgeBase.cs b/csharp/App/Backend/Services/AlarmKnowledgeBase.cs index 09507fc87..6573f36c4 100644 --- a/csharp/App/Backend/Services/AlarmKnowledgeBase.cs +++ b/csharp/App/Backend/Services/AlarmKnowledgeBase.cs @@ -4,7 +4,7 @@ namespace InnovEnergy.App.Backend.Services; /// /// Static knowledge base for Sinexcel and Growatt alarms. -/// Provides pre-defined diagnostics without requiring OpenAI API calls. +/// Provides pre-defined diagnostics without requiring Mistral API calls. /// Data sourced from vendor alarm documentation. /// public static class AlarmKnowledgeBase diff --git a/csharp/App/Backend/Services/DiagnosticService.cs b/csharp/App/Backend/Services/DiagnosticService.cs index ef9f0b22e..b2948f3de 100644 --- a/csharp/App/Backend/Services/DiagnosticService.cs +++ b/csharp/App/Backend/Services/DiagnosticService.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; namespace InnovEnergy.App.Backend.Services; /// -/// Calls OpenAI to generate plain-English diagnostics for errors/warnings. +/// Calls Mistral AI to generate plain-English diagnostics for errors/warnings. /// Caches responses in-memory keyed by error description so the same /// error code is only sent to the API once. /// @@ -22,30 +22,15 @@ public static class DiagnosticService public static void Initialize() { - var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "openAiConfig.json"); + var apiKey = Environment.GetEnvironmentVariable("MISTRAL_API_KEY"); - if (!File.Exists(configPath)) + if (string.IsNullOrWhiteSpace(apiKey)) { - // Fallback: look relative to the working directory (useful in dev) - configPath = Path.Combine("Resources", "openAiConfig.json"); - } - - if (!File.Exists(configPath)) - { - Console.Error.WriteLine("[DiagnosticService] openAiConfig.json not found – AI diagnostics disabled."); + Console.Error.WriteLine("[DiagnosticService] MISTRAL_API_KEY not set – AI diagnostics disabled."); return; } - var json = File.ReadAllText(configPath); - var config = JsonConvert.DeserializeObject(json); - - if (config is null || string.IsNullOrWhiteSpace(config.ApiKey)) - { - Console.Error.WriteLine("[DiagnosticService] ApiKey is empty – AI diagnostics disabled."); - return; - } - - _apiKey = config.ApiKey; + _apiKey = apiKey; Console.WriteLine("[DiagnosticService] initialised."); } @@ -56,7 +41,7 @@ public static class DiagnosticService /// /// Returns a diagnosis for . /// First checks the static AlarmKnowledgeBase for known Sinexcel/Growatt alarms. - /// Falls back to in-memory cache, then calls OpenAI only for unknown alarms. + /// Falls back to in-memory cache, then calls Mistral AI only for unknown alarms. /// public static async Task DiagnoseAsync(Int64 installationId, string errorDescription) { @@ -89,10 +74,10 @@ public static class DiagnosticService .Take(5) .ToList(); - // 5. Build prompt and call OpenAI API (only for unknown alarms) - Console.WriteLine($"[DiagnosticService] Calling OpenAI for unknown alarm: {errorDescription}"); + // 5. Build prompt and call Mistral API (only for unknown alarms) + Console.WriteLine($"[DiagnosticService] Calling Mistral for unknown alarm: {errorDescription}"); var prompt = BuildPrompt(errorDescription, productName, recentDescriptions); - var response = await CallOpenAiAsync(prompt); + var response = await CallMistralAsync(prompt); if (response is null) return null; @@ -121,17 +106,17 @@ Reply with ONLY valid JSON, no markdown: "; } - // ── OpenAI HTTP call ──────────────────────────────────────────── + // ── Mistral HTTP call ──────────────────────────────────────────── - private static readonly string OpenAiUrl = "https://api.openai.com/v1/chat/completions"; + private static readonly string MistralUrl = "https://api.mistral.ai/v1/chat/completions"; - private static async Task CallOpenAiAsync(string userPrompt) + private static async Task CallMistralAsync(string userPrompt) { try { var requestBody = new { - model = "gpt-4o-mini", // cost-efficient, fast; swap to "gpt-4" if quality needs tuning + model = "mistral-small-latest", // cost-efficient, fast; swap to "mistral-large-latest" if quality needs tuning messages = new[] { new { role = "user", content = userPrompt } @@ -140,19 +125,19 @@ Reply with ONLY valid JSON, no markdown: temperature = 0.2 // low temperature for factual consistency }; - var responseText = await OpenAiUrl + var responseText = await MistralUrl .SetHeader("Authorization", $"Bearer {_apiKey}") .SetHeader("Content-Type", "application/json") .PostJsonAsync(requestBody) .ReceiveString(); - // parse OpenAI envelope + // parse Mistral envelope (same structure as OpenAI) var envelope = JsonConvert.DeserializeObject(responseText); var content = (string?) envelope?.choices?[0]?.message?.content; if (string.IsNullOrWhiteSpace(content)) { - Console.Error.WriteLine("[DiagnosticService] OpenAI returned empty content."); + Console.Error.WriteLine("[DiagnosticService] Mistral returned empty content."); return null; } @@ -175,11 +160,6 @@ Reply with ONLY valid JSON, no markdown: // ── config / response models ──────────────────────────────────────────────── -public class OpenAiConfig -{ - public string ApiKey { get; set; } = ""; -} - public class DiagnosticResponse { public string Explanation { get; set; } = "";