integrated reviewed AI alarm diagnosis
This commit is contained in:
parent
127e16eb34
commit
78d67d77fd
|
|
@ -5,3 +5,5 @@
|
|||
**/.idea/
|
||||
**/.env
|
||||
.claude/
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
|
|
|
|||
|
|
@ -806,7 +806,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 Mistral again.
|
||||
/// do not hit the AI provider again.
|
||||
/// </summary>
|
||||
[HttpGet(nameof(DiagnoseError))]
|
||||
public async Task<ActionResult<DiagnosticResponse>> DiagnoseError(Int64 installationId, string errorDescription, Token authToken)
|
||||
|
|
@ -839,8 +839,11 @@ public class Controller : ControllerBase
|
|||
/// Remove this endpoint in production if not needed.
|
||||
/// </summary>
|
||||
[HttpGet(nameof(TestAlarmKnowledgeBase))]
|
||||
public ActionResult TestAlarmKnowledgeBase()
|
||||
public ActionResult TestAlarmKnowledgeBase(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var testCases = new[]
|
||||
{
|
||||
// Sinexcel alarms (keys match SinexcelRecord.Api.cs property names)
|
||||
|
|
@ -855,7 +858,7 @@ public class Controller : ControllerBase
|
|||
"BmsFault",
|
||||
"OverTemperature",
|
||||
"AFCI Fault",
|
||||
// Unknown alarm (should return null - would call Mistral)
|
||||
// Unknown alarm (should return null - would call AI)
|
||||
"Some unknown alarm XYZ123"
|
||||
};
|
||||
|
||||
|
|
@ -867,7 +870,7 @@ public class Controller : ControllerBase
|
|||
{
|
||||
Alarm = alarm,
|
||||
FoundInKnowledgeBase = diagnosis != null,
|
||||
Explanation = diagnosis?.Explanation ?? "NOT FOUND - Would call Mistral API",
|
||||
Explanation = diagnosis?.Explanation ?? "NOT FOUND - Would call AI API",
|
||||
CausesCount = diagnosis?.Causes.Count ?? 0,
|
||||
NextStepsCount = diagnosis?.NextSteps.Count ?? 0
|
||||
});
|
||||
|
|
@ -878,45 +881,47 @@ public class Controller : ControllerBase
|
|||
TestTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
TotalTests = testCases.Length,
|
||||
FoundInKnowledgeBase = results.Count(r => ((dynamic)r).FoundInKnowledgeBase),
|
||||
WouldCallMistral = results.Count(r => !((dynamic)r).FoundInKnowledgeBase),
|
||||
WouldCallAi = results.Count(r => !((dynamic)r).FoundInKnowledgeBase),
|
||||
Results = results
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test endpoint for the full AI diagnostic flow (knowledge base + Mistral API).
|
||||
/// No auth required. Remove before production.
|
||||
/// Usage: GET /api/TestDiagnoseError?errorDescription=SomeAlarm
|
||||
/// Test endpoint for the full AI diagnostic flow (knowledge base + AI API).
|
||||
/// Admin-only. Usage: GET /api/TestDiagnoseError?errorDescription=SomeAlarm
|
||||
/// </summary>
|
||||
[HttpGet(nameof(TestDiagnoseError))]
|
||||
public async Task<ActionResult> TestDiagnoseError(string errorDescription = "AbnormalGridVoltage", string language = "en")
|
||||
public async Task<ActionResult> TestDiagnoseError(Token authToken, string errorDescription = "AbnormalGridVoltage", string language = "en")
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
// 1. Try static lookup (KB for English, pre-generated translations for others)
|
||||
var staticResult = DiagnosticService.TryGetTranslation(errorDescription, language);
|
||||
if (staticResult is not null)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
Source = "KnowledgeBase",
|
||||
Alarm = errorDescription,
|
||||
MistralEnabled = DiagnosticService.IsEnabled,
|
||||
Source = "KnowledgeBase",
|
||||
Alarm = errorDescription,
|
||||
AiEnabled = DiagnosticService.IsEnabled,
|
||||
staticResult.Explanation,
|
||||
staticResult.Causes,
|
||||
staticResult.NextSteps
|
||||
});
|
||||
}
|
||||
|
||||
// 2. If not found, try Mistral with the correct language
|
||||
// 2. If not found, try AI with the correct language
|
||||
if (!DiagnosticService.IsEnabled)
|
||||
return Ok(new { Source = "None", Alarm = errorDescription, Message = "Not in knowledge base and Mistral API key not configured." });
|
||||
return Ok(new { Source = "None", Alarm = errorDescription, Message = "Not in knowledge base and AI API key not configured." });
|
||||
|
||||
var aiResult = await DiagnosticService.TestCallMistralAsync(errorDescription, language);
|
||||
if (aiResult is null)
|
||||
return Ok(new { Source = "MistralFailed", Alarm = errorDescription, Message = "Mistral API call failed or returned empty." });
|
||||
return Ok(new { Source = "AiFailed", Alarm = errorDescription, Message = "AI API call failed or returned empty." });
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Source = "MistralAI",
|
||||
Source = "Ai",
|
||||
Alarm = errorDescription,
|
||||
aiResult.Explanation,
|
||||
aiResult.Causes,
|
||||
|
|
@ -2012,50 +2017,71 @@ public class Controller : ControllerBase
|
|||
// ── Alarm Review Campaign ────────────────────────────────────────────────
|
||||
|
||||
[HttpPost(nameof(SendTestAlarmReview))]
|
||||
public async Task<ActionResult> SendTestAlarmReview()
|
||||
public async Task<ActionResult> SendTestAlarmReview(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
await AlarmReviewService.SendTestBatchAsync();
|
||||
return Ok(new { message = "Test review email sent to liu@inesco.energy. Check your inbox." });
|
||||
}
|
||||
|
||||
[HttpPost(nameof(StartAlarmReviewCampaign))]
|
||||
public ActionResult StartAlarmReviewCampaign()
|
||||
public ActionResult StartAlarmReviewCampaign(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
AlarmReviewService.StartCampaign();
|
||||
return Ok(new { message = "Alarm review campaign started." });
|
||||
}
|
||||
|
||||
[HttpPost(nameof(StopAlarmReviewCampaign))]
|
||||
public ActionResult StopAlarmReviewCampaign()
|
||||
public ActionResult StopAlarmReviewCampaign(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
AlarmReviewService.StopCampaign();
|
||||
return Ok(new { message = "Campaign paused — progress preserved. Use ResumeAlarmReviewCampaign to restart timers." });
|
||||
}
|
||||
|
||||
[HttpPost(nameof(ResumeAlarmReviewCampaign))]
|
||||
public ActionResult ResumeAlarmReviewCampaign()
|
||||
public ActionResult ResumeAlarmReviewCampaign(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
AlarmReviewService.ResumeCampaign();
|
||||
return Ok(new { message = "Campaign resumed — timers restarted from existing progress." });
|
||||
}
|
||||
|
||||
[HttpPost(nameof(ResetAlarmReviewCampaign))]
|
||||
public ActionResult ResetAlarmReviewCampaign()
|
||||
public ActionResult ResetAlarmReviewCampaign(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
AlarmReviewService.ResetCampaign();
|
||||
return Ok(new { message = "Campaign fully reset — all progress deleted. Use StartAlarmReviewCampaign to begin again." });
|
||||
}
|
||||
|
||||
[HttpGet(nameof(CorrectAlarm))]
|
||||
public ActionResult CorrectAlarm(int batch, string key)
|
||||
public ActionResult CorrectAlarm(Token authToken, int batch, string key)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var html = AlarmReviewService.GetCorrectionPage(batch, key);
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
[HttpPost(nameof(ApplyAlarmCorrection))]
|
||||
public ActionResult ApplyAlarmCorrection([FromBody] AlarmCorrectionRequest req)
|
||||
public ActionResult ApplyAlarmCorrection(Token authToken, [FromBody] AlarmCorrectionRequest req)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
if (req == null) return BadRequest();
|
||||
var correction = new DiagnosticResponse
|
||||
{
|
||||
|
|
@ -2068,16 +2094,22 @@ public class Controller : ControllerBase
|
|||
}
|
||||
|
||||
[HttpGet(nameof(ReviewAlarms))]
|
||||
public ActionResult ReviewAlarms(int batch, string reviewer)
|
||||
public ActionResult ReviewAlarms(Token authToken, int batch, string reviewer)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var html = AlarmReviewService.GetReviewPage(batch, reviewer);
|
||||
if (html is null) return NotFound("Batch not found or reviewer not recognised.");
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
[HttpPost(nameof(SubmitAlarmReview))]
|
||||
public async Task<ActionResult> SubmitAlarmReview(int batch, string? reviewer, [FromBody] List<ReviewFeedback>? feedbacks)
|
||||
public async Task<ActionResult> SubmitAlarmReview(Token authToken, int batch, string? reviewer, [FromBody] List<ReviewFeedback>? feedbacks)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
// Batch 0 = test mode — run dry-run synthesis and return preview HTML (nothing is saved)
|
||||
if (batch == 0)
|
||||
{
|
||||
|
|
@ -2091,14 +2123,20 @@ public class Controller : ControllerBase
|
|||
}
|
||||
|
||||
[HttpGet(nameof(GetAlarmReviewStatus))]
|
||||
public ActionResult GetAlarmReviewStatus()
|
||||
public ActionResult GetAlarmReviewStatus(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
return Ok(AlarmReviewService.GetStatus());
|
||||
}
|
||||
|
||||
[HttpGet(nameof(DownloadCheckedKnowledgeBase))]
|
||||
public ActionResult DownloadCheckedKnowledgeBase()
|
||||
public ActionResult DownloadCheckedKnowledgeBase(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var content = AlarmReviewService.GetCheckedFileContent();
|
||||
if (content is null) return NotFound("AlarmKnowledgeBaseChecked.cs has not been generated yet.");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
{
|
||||
"AbnormalGridVoltage": {
|
||||
"Explanation": "Der Wechselrichter hat festgestellt, dass die Netzspannung außerhalb des zulässigen Bereichs liegt. Das System benötigt manuellen Eingriff zur Wiederherstellung.",
|
||||
"Causes": [
|
||||
|
|
@ -28,15 +28,15 @@
|
|||
]
|
||||
},
|
||||
"InvertedSequenceOfGridVoltage": {
|
||||
"Explanation": "Die Phasenreihenfolge der dreiphasigen Netzspannung ist vertauscht. Dies ist ein Verdrahtungsproblem, das einen sicheren Betrieb verhindert.",
|
||||
"Explanation": "Die Reihenfolge der drei Stromphasen vom Netz ist falsch angeschlossen. Das System kann nicht sicher arbeiten.",
|
||||
"Causes": [
|
||||
"Falsche Verdrahtung der Netzphasen während der Installation (L1, L2, L3 vertauscht)",
|
||||
"Nachträgliche Verdrahtungsarbeiten ohne Überprüfung der Phasenfolge"
|
||||
"Falsche Verdrahtung der Netzphasen (L1, L2, L3) bei der Installation",
|
||||
"Nachträgliche Änderungen an der Verdrahtung ohne Prüfung der Phasenfolge"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Schalten Sie das gesamte System sicher aus, bevor Sie die Verdrahtung berühren",
|
||||
"Vertauschen Sie zwei der drei Phasenleitungen an der Netzverbindung, um die Reihenfolge zu korrigieren",
|
||||
"Schalten Sie das System wieder ein und überprüfen Sie, ob die Warnung behoben ist"
|
||||
"Tauschen Sie zwei der drei Phasenleitungen an der Netzverbindung, um die Reihenfolge zu korrigieren",
|
||||
"Schalten Sie das System wieder ein und prüfen Sie, ob die Warnung verschwunden ist"
|
||||
]
|
||||
},
|
||||
"GridVoltagePhaseLoss": {
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
]
|
||||
},
|
||||
"ExcessiveRadiatorTemperature": {
|
||||
"Explanation": "Die Temperatur des Kühlkörpers (Radiator) des Wechselrichters ist zu hoch. Der Kühlkörper dient dazu, Wärme während des Betriebs abzuführen.",
|
||||
"Explanation": "Der Kühlkörper des Wechselrichters ist zu heiß, weil die Wärme nicht richtig abgeführt wird.",
|
||||
"Causes": [
|
||||
"Verstopfte oder blockierte Lüftungsschlitze verhindern die Wärmeabfuhr",
|
||||
"Ausfall des Kühlgebläses reduziert die Luftzirkulation",
|
||||
|
|
@ -145,8 +145,7 @@
|
|||
"NextSteps": [
|
||||
"Reinigen Sie die Lüftungsschlitze und Staubfilter – Staubansammlungen sind eine häufige Ursache",
|
||||
"Überprüfen Sie, ob das Kühlgebläse läuft (Hören Sie auf Gebläsegeräusche während des Betriebs)",
|
||||
"Reduzieren Sie die Last vorübergehend, um die Wärmeentwicklung zu verringern",
|
||||
"Reparieren oder ersetzen Sie das Gebläse, falls es defekt ist, und starten Sie den Wechselrichter neu"
|
||||
"Reduzieren Sie die Last vorübergehend, um die Wärmeentwicklung zu verringern"
|
||||
]
|
||||
},
|
||||
"PcbOvertemperature": {
|
||||
|
|
@ -541,13 +540,14 @@
|
|||
"Battery2OverloadTimeout": {
|
||||
"Explanation": "Batterie 2 läuft seit zu langer Zeit unter Überlast.",
|
||||
"Causes": [
|
||||
"Dauerhafte hohe Last, die die Entladerating von Batterie 2 überschreitet",
|
||||
"Dauerhafte hohe Last, die die Entladeleistung von Batterie 2 überschreitet",
|
||||
"Batterie 2 ist degradiert und kann weniger Leistung bereitstellen"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Den Gesamtstromverbrauch reduzieren",
|
||||
"Prüfen, ob Batterie 2 für die Lastanforderungen richtig dimensioniert ist",
|
||||
"Nach Lastreduzierung den Wechselrichter neu starten"
|
||||
"Nach Lastreduzierung den Wechselrichter neu starten",
|
||||
"Bitte überprüfen Sie die Einstellungen zur Batterie (z. B. Lade-/Entladestrom)"
|
||||
]
|
||||
},
|
||||
"Battery2SoftStartFailure": {
|
||||
|
|
@ -967,9 +967,9 @@
|
|||
]
|
||||
},
|
||||
"Pv3ReverseConnection": {
|
||||
"Explanation": "PV-String 3 ist mit vertauschter Polarität angeschlossen. Dies ist ein Verdrahtungsfehler, der vor dem Betrieb behoben werden muss.",
|
||||
"Explanation": "PV-String 3 ist mit vertauschter Polarität angeschlossen. Dieser Verdrahtungsfehler muss vor dem Betrieb behoben werden.",
|
||||
"Causes": [
|
||||
"Positive und negative Kabel von PV-String 3 wurden während der Installation vertauscht",
|
||||
"Positive und negative Kabel von PV-String 3 wurden womöglich während der Installation vertauscht",
|
||||
"Falsche Kabelverbindung am DC-Eingang des Wechselrichters"
|
||||
],
|
||||
"NextSteps": [
|
||||
|
|
@ -1247,12 +1247,13 @@
|
|||
]
|
||||
},
|
||||
"InverterOverloadTimeout": {
|
||||
"Explanation": "Der Wechselrichter war zu lange überlastet und hat sich abgeschaltet.",
|
||||
"Explanation": "Der Wechselrichter war zu lange überlastet und hat sich automatisch abgeschaltet.",
|
||||
"Causes": [
|
||||
"Dauerhafte Überlastung, die die Kurzzeit-Überlastfähigkeit des Wechselrichters überschreitet",
|
||||
"Der Wechselrichter ist für die tatsächliche Last zu klein dimensioniert"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Die sinnvolle Aufteilung der Lasten zwischen Notstrom- und normalem Hausnetz überprüfen.",
|
||||
"Die angeschlossene Last dauerhaft reduzieren",
|
||||
"Falls die Last notwendig ist, auf einen größeren Wechselrichter umsteigen",
|
||||
"Die Ursache beheben und den Wechselrichter neu starten"
|
||||
|
|
@ -1265,6 +1266,7 @@
|
|||
"Ein neues leistungsstarkes Gerät wurde hinzugefügt, das die Systemleistung übersteigt"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Die sinnvolle Aufteilung der Lasten zwischen Notstrom- und normalem Hausnetz überprüfen.",
|
||||
"Last reduzieren, indem nicht essentielle Geräte ausgeschaltet werden",
|
||||
"Nutzung leistungsstarker Geräte staffeln und den Wechselrichter neu starten"
|
||||
]
|
||||
|
|
@ -1295,16 +1297,15 @@
|
|||
]
|
||||
},
|
||||
"Dsp1ParameterSettingFault": {
|
||||
"Explanation": "DSP 1 (digitaler Signalprozessor) hat eine falsche Parameterkonfiguration erkannt.",
|
||||
"Explanation": "Der Wechselrichter hat eine falsche Einstellung in seinen internen Parametern erkannt.",
|
||||
"Causes": [
|
||||
"Ein oder mehrere Wechselrichterparameter sind außerhalb des zulässigen Bereichs eingestellt",
|
||||
"Firmware-Korruption beeinflusst die Parameterspeicherung",
|
||||
"Konfigurationsinkonsistenz nach einem Firmware-Update"
|
||||
"Ein oder mehrere Parameter des Wechselrichters liegen außerhalb des erlaubten Bereichs.",
|
||||
"Die Firmware ist beschädigt und beeinflusst die Speicherung der Einstellungen.",
|
||||
"Nach einem Firmware-Update stimmen die Einstellungen nicht mehr überein."
|
||||
],
|
||||
"NextSteps": [
|
||||
"Alle Wechselrichter-Parameter überprüfen und eventuell ungültige Werte korrigieren",
|
||||
"Parameter auf Werkseinstellungen zurücksetzen, falls unsicher über die richtigen Werte",
|
||||
"Die Ursache beheben und den Wechselrichter neu starten"
|
||||
"Überprüfen Sie alle Parameter des Wechselrichters und korrigieren Sie ungültige Werte.",
|
||||
"Setzen Sie die Parameter auf Werkseinstellungen zurück, falls Sie unsicher sind."
|
||||
]
|
||||
},
|
||||
"Dsp2ParameterSettingFault": {
|
||||
|
|
@ -1568,14 +1569,15 @@
|
|||
]
|
||||
},
|
||||
"ReverseMeterConnection": {
|
||||
"Explanation": "Der Stromzähler ist falsch installiert oder verdrahtet. Die Zählerstände (Import/Export) sind bis zur Korrektur ungenau.",
|
||||
"Explanation": "Der Stromzähler ist falsch angeschlossen, sodass die Messwerte (Strombezug/Einspeisung) nicht stimmen.",
|
||||
"Causes": [
|
||||
"Der Stromwandler (CT) ist in die falsche Richtung installiert",
|
||||
"Die L- und N-Leitungen des Zählers sind bei der Installation vertauscht"
|
||||
"Der Stromwandler (CT) ist in die falsche Richtung eingebaut",
|
||||
"Die L- und N-Leitungen des Zählers wurden vertauscht",
|
||||
"Andere Messwandler in der Nähe stören – mindestens 30 cm Abstand halten"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Verlassen Sie sich nicht auf die Zählerstände, bis die Korrektur erfolgt ist",
|
||||
"Kontaktieren Sie Ihren Installateur oder einen qualifizierten Elektriker, um den Stromwandler oder die Zählerverkabelung zu korrigieren"
|
||||
"Kontaktieren Sie Ihren Installateur oder einen qualifizierten Elektriker, um den Stromwandler oder die Zählerverkabelung zu prüfen"
|
||||
]
|
||||
},
|
||||
"InverterSealPulse": {
|
||||
|
|
@ -2121,7 +2123,7 @@
|
|||
]
|
||||
},
|
||||
"LithiumBatteryOverload": {
|
||||
"Explanation": "Der Überlastschutz der Lithiumbatterie wurde aktiviert – die Last entnimmt mehr Strom, als die Batterie abgeben kann.",
|
||||
"Explanation": "Der Überlastschutz der Batterie wurde aktiviert – die Last entnimmt mehr Strom, als die Batterie abgeben kann.",
|
||||
"Causes": [
|
||||
"Die Gesamtlastleistung überschreitet die maximale Entladeleistung der Batterie",
|
||||
"Hochstrom beim Einschalten großer Motoren oder Kompressoren übersteigt vorübergehend die Batteriegrenzen"
|
||||
|
|
@ -2220,6 +2222,19 @@
|
|||
"Reparieren oder entfernen Sie den überlastenden Verbraucher, bevor Sie den Wechselrichter neu starten"
|
||||
]
|
||||
},
|
||||
"OffGridBusVoltageTooLow": {
|
||||
"Explanation": "Die Gleichspannung im Inselbetrieb ist zu stark abgesunken, um einen stabilen Betrieb aufrechtzuerhalten.",
|
||||
"Causes": [
|
||||
"Batterieladestand zu niedrig",
|
||||
"Zu hohe Last am Inselausgang",
|
||||
"Defekt oder lockere Verbindung in der DC-Bus-Verdrahtung"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Last am Inselausgang reduzieren",
|
||||
"Batterieladestand prüfen und ggf. aufladen",
|
||||
"DC-Bus-Verdrahtung auf lockere Verbindungen oder Schäden überprüfen"
|
||||
]
|
||||
},
|
||||
"OffGridOutputOverload": {
|
||||
"Explanation": "Der netzunabhängige (EPS/Backup)-Ausgang ist überlastet — es wird mehr Strom angefordert, als der Wechselrichter im Backup-Modus liefern kann.",
|
||||
"Causes": [
|
||||
|
|
@ -2805,18 +2820,5 @@
|
|||
"Messen Sie die tatsächliche DC-Spannung, bevor Sie wieder anschließen",
|
||||
"Überprüfen Sie das String-Design und reduzieren Sie gegebenenfalls die Anzahl der Module in Reihe, um die Wechselrichter-Spannungsgrenzen einzuhalten"
|
||||
]
|
||||
},
|
||||
"OffGridBusVoltageTooLow": {
|
||||
"Explanation": "Die Gleichspannung im Inselbetrieb ist zu stark abgesunken, um einen stabilen Betrieb aufrechtzuerhalten.",
|
||||
"Causes": [
|
||||
"Batterieladestand zu niedrig",
|
||||
"Zu hohe Last am Inselausgang",
|
||||
"Defekt oder lockere Verbindung in der DC-Bus-Verdrahtung"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Last am Inselausgang reduzieren",
|
||||
"Batterieladestand prüfen und ggf. aufladen",
|
||||
"DC-Bus-Verdrahtung auf lockere Verbindungen oder Schäden überprüfen"
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -279,6 +279,10 @@ public static class AlarmReviewService
|
|||
|
||||
if (current.Synthesized)
|
||||
{
|
||||
// Campaign is fully reviewed → nothing left to do. Without this guard the
|
||||
// recovery branch below re-fires the admin completion email every workday.
|
||||
if (progress.Batches.Count * BatchSize >= AllAlarmKeys.Length) return;
|
||||
|
||||
// Next batch is sent immediately after synthesis — only act here as a safety net
|
||||
// in case the server restarted before SendNextBatchAsync could run.
|
||||
var nextAlreadySent = progress.Batches.Count > current.BatchNumber;
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -4,18 +4,29 @@ generate_alarm_translations.py
|
|||
|
||||
Post-campaign script: reads AlarmTranslationsChecked.de.json (the reviewed and
|
||||
AI-synthesized German content), translates into English, French, and Italian,
|
||||
and writes:
|
||||
and writes preview files for review BEFORE replacing the live translations:
|
||||
|
||||
Resources/AlarmTranslations.de.json ← replace with reviewed German
|
||||
Resources/AlarmTranslations.en.json ← back-translated from German
|
||||
Resources/AlarmTranslations.fr.json ← translated from German
|
||||
Resources/AlarmTranslations.it.json ← translated from German
|
||||
Services/AlarmKnowledgeBase.cs ← updated English source (keeps same structure)
|
||||
Resources/AlarmTranslationsChecked.en.json ← NEW (back-translated from German)
|
||||
Resources/AlarmTranslationsChecked.fr.json ← NEW (translated from German)
|
||||
Resources/AlarmTranslationsChecked.it.json ← NEW (translated from German)
|
||||
Services/AlarmKnowledgeBase.cs ← updated in-place (review via `git diff`)
|
||||
|
||||
Resources/AlarmTranslationsChecked.de.json is the INPUT and is not modified.
|
||||
Live files Resources/AlarmTranslations.{de,fr,it}.json are NOT overwritten —
|
||||
review the *Checked* files, then manually copy them onto the live names when ready:
|
||||
|
||||
cp Resources/AlarmTranslationsChecked.de.json Resources/AlarmTranslations.de.json
|
||||
cp Resources/AlarmTranslationsChecked.en.json Resources/AlarmTranslations.en.json
|
||||
cp Resources/AlarmTranslationsChecked.fr.json Resources/AlarmTranslations.fr.json
|
||||
cp Resources/AlarmTranslationsChecked.it.json Resources/AlarmTranslations.it.json
|
||||
|
||||
Run this AFTER the review campaign is complete:
|
||||
export MISTRAL_API_KEY=your_key_here
|
||||
cd csharp/App/Backend
|
||||
python3 generate_alarm_translations.py
|
||||
|
||||
The script reads MISTRAL_API_KEY from the environment, falling back to the same
|
||||
.env file the C# backend uses (csharp/App/Backend/.env). No `export` needed if
|
||||
the .env file is in place.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
@ -23,7 +34,6 @@ import os
|
|||
import re
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
from typing import Optional
|
||||
import requests
|
||||
|
||||
|
|
@ -245,18 +255,17 @@ def main():
|
|||
print(f"ERROR: {CHECKED_FILE} not found. Run the review campaign first.")
|
||||
sys.exit(1)
|
||||
|
||||
with open(CHECKED_FILE, "r", encoding="utf-8") as f:
|
||||
# utf-8-sig strips the BOM that the C# AlarmReviewService writes via Encoding.UTF8
|
||||
with open(CHECKED_FILE, "r", encoding="utf-8-sig") as f:
|
||||
german_source = json.load(f)
|
||||
|
||||
alarm_keys = list(german_source.keys())
|
||||
print(f"Loaded {len(alarm_keys)} alarms from {CHECKED_FILE}.")
|
||||
|
||||
# Step 1: copy reviewed German as the new de.json
|
||||
de_out = os.path.join(RESOURCES_DIR, "AlarmTranslations.de.json")
|
||||
shutil.copy(CHECKED_FILE, de_out)
|
||||
print(f"\n✓ Copied reviewed German → {de_out}")
|
||||
# The reviewed German JSON is already at AlarmTranslationsChecked.de.json — no copy needed.
|
||||
# User will manually replace AlarmTranslations.de.json after reviewing all four Checked files.
|
||||
|
||||
# Step 2: translate to en, fr, it
|
||||
# Translate to en, fr, it → write to AlarmTranslationsChecked.{lang}.json (preview names)
|
||||
all_translations = {} # lang_code → {key → entry}
|
||||
for lang_code, lang_name in TARGET_LANGUAGES.items():
|
||||
print(f"\n── Translating to {lang_name} ({lang_code}) ──")
|
||||
|
|
@ -296,7 +305,7 @@ def main():
|
|||
time.sleep(1)
|
||||
|
||||
all_translations[lang_code] = translations
|
||||
out_file = os.path.join(RESOURCES_DIR, f"AlarmTranslations.{lang_code}.json")
|
||||
out_file = os.path.join(RESOURCES_DIR, f"AlarmTranslationsChecked.{lang_code}.json")
|
||||
with open(out_file, "w", encoding="utf-8") as f:
|
||||
json.dump(translations, f, ensure_ascii=False, indent=2)
|
||||
print(f" ✓ Wrote {len(translations)} entries → {out_file}")
|
||||
|
|
@ -312,8 +321,17 @@ def main():
|
|||
else:
|
||||
print(" Skipped — en.json not generated or AlarmKnowledgeBase.cs not found.")
|
||||
|
||||
print("\n✓ Done. Review the output files before deploying.")
|
||||
print(" Next: cd csharp/App/Backend && dotnet build && ./deploy.sh")
|
||||
print("\n✓ Done. Review these preview files before replacing the live ones:")
|
||||
print(f" - {RESOURCES_DIR}/AlarmTranslationsChecked.de.json (reviewed German — input, unchanged)")
|
||||
print(f" - {RESOURCES_DIR}/AlarmTranslationsChecked.en.json (new)")
|
||||
print(f" - {RESOURCES_DIR}/AlarmTranslationsChecked.fr.json (new)")
|
||||
print(f" - {RESOURCES_DIR}/AlarmTranslationsChecked.it.json (new)")
|
||||
print(f" - {KNOWLEDGE_BASE} (overwritten — review with `git diff`)")
|
||||
print("\nWhen satisfied:")
|
||||
print(" for lang in de en fr it; do")
|
||||
print(f" cp {RESOURCES_DIR}/AlarmTranslationsChecked.$lang.json {RESOURCES_DIR}/AlarmTranslations.$lang.json")
|
||||
print(" done")
|
||||
print(" dotnet build && ./deploy.sh")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
patch_missing_alarms.py
|
||||
|
||||
Re-translates specific keys that failed during a previous run of
|
||||
generate_alarm_translations.py (e.g. due to LLM JSON-format glitches),
|
||||
then regenerates AlarmKnowledgeBase.cs with the now-complete English set.
|
||||
|
||||
Translates one key per API call to dodge the multi-key JSON formatting
|
||||
issue that caused the original failures.
|
||||
|
||||
Edit MISSING below to set which keys to retry per language, then run:
|
||||
cd csharp/App/Backend
|
||||
python3 patch_missing_alarms.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from generate_alarm_translations import (
|
||||
translate_batch,
|
||||
parse_kb_key_sections,
|
||||
write_knowledge_base_cs,
|
||||
load_env_file,
|
||||
CHECKED_FILE,
|
||||
KNOWLEDGE_BASE,
|
||||
RESOURCES_DIR,
|
||||
TARGET_LANGUAGES,
|
||||
)
|
||||
|
||||
# Keys that failed during the 2026-04-28 run.
|
||||
# Update this dict if a new run produces different failures.
|
||||
MISSING = {
|
||||
"en": [
|
||||
"DcBusOvervoltage",
|
||||
"DcBusUndervoltage",
|
||||
"DcBusVoltageUnbalance",
|
||||
"BusSlowOvervoltage",
|
||||
"HardwareBusOvervoltage",
|
||||
],
|
||||
"it": [
|
||||
"NtcTemperatureSensorBroken",
|
||||
"SyncSignalAbnormal",
|
||||
"GridStartupConditionsNotMet",
|
||||
"BatteryCommunicationFailure",
|
||||
"BatteryDisconnected",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
api_key = os.environ.get("MISTRAL_API_KEY", "").strip()
|
||||
if not api_key:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
api_key = load_env_file(os.path.join(script_dir, ".env")).get("MISTRAL_API_KEY", "").strip()
|
||||
if not api_key:
|
||||
print("ERROR: MISTRAL_API_KEY not found in environment or .env file.")
|
||||
sys.exit(1)
|
||||
print("MISTRAL_API_KEY loaded.")
|
||||
|
||||
with open(CHECKED_FILE, encoding="utf-8-sig") as f:
|
||||
de = json.load(f)
|
||||
|
||||
en_translations = None
|
||||
|
||||
for lang_code, missing_keys in MISSING.items():
|
||||
lang_name = TARGET_LANGUAGES[lang_code]
|
||||
out_file = os.path.join(RESOURCES_DIR, f"AlarmTranslationsChecked.{lang_code}.json")
|
||||
with open(out_file, encoding="utf-8") as f:
|
||||
existing = json.load(f)
|
||||
|
||||
# Idempotent: only translate keys that are still genuinely missing from the JSON.
|
||||
actually_missing = [
|
||||
k for k in missing_keys
|
||||
if k in de and (k not in existing or not existing[k].get("Explanation"))
|
||||
]
|
||||
if not actually_missing:
|
||||
print(f"\n── {lang_name} ({lang_code}) already complete ({len(existing)} entries) — skipping translation ──")
|
||||
else:
|
||||
print(f"\n── Patching {lang_name} ({lang_code}) — {len(actually_missing)} keys ──")
|
||||
translated = {}
|
||||
for key in actually_missing:
|
||||
print(f" {key}")
|
||||
result = translate_batch(api_key, {key: de[key]}, lang_name)
|
||||
if result and key in result:
|
||||
r = result[key]
|
||||
translated[key] = {
|
||||
"Explanation": r.get("Explanation", ""),
|
||||
"Causes": r.get("Causes", []),
|
||||
"NextSteps": r.get("NextSteps", []),
|
||||
}
|
||||
snippet = r.get("Explanation", "")[:80]
|
||||
print(f" OK: {snippet}{'...' if len(r.get('Explanation','')) > 80 else ''}")
|
||||
else:
|
||||
print(f" FAILED: {key}")
|
||||
|
||||
existing.update(translated)
|
||||
with open(out_file, "w", encoding="utf-8") as f:
|
||||
json.dump(existing, f, ensure_ascii=False, indent=2)
|
||||
print(f" ✓ Wrote {len(existing)} total entries → {out_file}")
|
||||
|
||||
if lang_code == "en":
|
||||
en_translations = existing
|
||||
|
||||
if en_translations is not None and os.path.exists(KNOWLEDGE_BASE):
|
||||
print("\n── Regenerating AlarmKnowledgeBase.cs ──")
|
||||
key_sections = parse_kb_key_sections(KNOWLEDGE_BASE)
|
||||
write_knowledge_base_cs(KNOWLEDGE_BASE, en_translations, key_sections)
|
||||
|
||||
print("\n✓ Patch done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue