Switch AI diagnostics from OpenAI to Mistral and use .env for API key

- Changed API endpoint to api.mistral.ai, model to mistral-small-latest
- Replaced openAiConfig.json with .env file for secure API key storage
- Added .env loader in Program.cs, added .env to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yinyin Liu 2026-02-12 07:45:16 +01:00
parent e7f8aacc34
commit 68f4006f55
7 changed files with 53 additions and 45 deletions

2
.gitignore vendored
View File

@ -3,4 +3,4 @@
**/obj
*.DotSettings.user
**/.idea/
**/.env

View File

@ -43,6 +43,9 @@
<None Update="Resources/s3cmd.py">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update=".env">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Remove="DbBackups\db-1698326303.sqlite" />
<None Remove="DbBackups\db-1698327045.sqlite" />
<None Remove="DbBackups\db-1699453468.sqlite" />

View File

@ -741,7 +741,7 @@ public class Controller : ControllerBase
/// <summary>
/// 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.
/// </summary>
[HttpGet(nameof(DiagnoseError))]
public async Task<ActionResult<DiagnosticResponse>> 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
});
}

View File

@ -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",

View File

@ -1,3 +0,0 @@
{
"ApiKey": "sk-your-openai-api-key-here"
}

View File

@ -4,7 +4,7 @@ namespace InnovEnergy.App.Backend.Services;
/// <summary>
/// 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.
/// </summary>
public static class AlarmKnowledgeBase

View File

@ -7,7 +7,7 @@ using Newtonsoft.Json;
namespace InnovEnergy.App.Backend.Services;
/// <summary>
/// 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.
/// </summary>
@ -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<OpenAiConfig>(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
/// <summary>
/// Returns a diagnosis for <paramref name="errorDescription"/>.
/// 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.
/// </summary>
public static async Task<DiagnosticResponse?> 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<DiagnosticResponse?> CallOpenAiAsync(string userPrompt)
private static async Task<DiagnosticResponse?> 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<dynamic>(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; } = "";