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:
parent
e7f8aacc34
commit
68f4006f55
|
|
@ -3,4 +3,4 @@
|
|||
**/obj
|
||||
*.DotSettings.user
|
||||
**/.idea/
|
||||
|
||||
**/.env
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"ApiKey": "sk-your-openai-api-key-here"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; } = "";
|
||||
|
|
|
|||
Loading…
Reference in New Issue