feat: KI-Rohantwort im Verlauf-Tab, echte Fehlermeldung, GUI-Log-Verzeichnis, Prompt-Härtung
Diagnose-Verbesserungen rund um die KI-Verarbeitung: - Verlauf-Tab zeigt die vollständige KI-Rohantwort des ausgewählten Versuchs an (neues Feld "KI-Rohantwort"). Die technische Original-Fehlermeldung ist zusätzlich als Tooltip der Fehlerursache verfügbar. - AiFailureMessageTranslator: eigene, freundliche Meldung für strukturell nicht verwertbare KI-Antworten (NOT_JSON_OBJECT/INVALID_JSON u.a.); der bisher generische Fallback zeigt jetzt den echten technischen Grund an, statt ihn zu verbergen. - Log-Verzeichnis wird im GUI-Modus zur Laufzeit auf das in der geladenen Konfiguration konfigurierte Verzeichnis umgestellt (RuntimeLogDirectoryReconfigurer, Best-Effort, Log4j2-Reconfigure). Bisher landeten GUI-Logs im user.home-Fallback, weil die GUI ohne --config startet. Der headless Pfad bleibt unverändert. - Standard-Prompt: explizit keine Markdown-Codeblöcke/Zäune, nur reines JSON; Unsicherheiten gehören ins reasoning-Feld. Tests fuer alle vier Punkte ergaenzt/aktualisiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
-2
@@ -25,7 +25,9 @@ public final class AiFailureMessageTranslator {
|
||||
* technische Fehlerbeschreibung.
|
||||
* <p>
|
||||
* Ist {@code technicalMessage} {@code null} oder leer, wird der allgemeine
|
||||
* Fallback-Text zurückgegeben.
|
||||
* Fallback-Text zurückgegeben. Für eine nicht leere, aber keinem Muster zuordenbare
|
||||
* Meldung wird die technische Originalmeldung sichtbar angehängt, damit die tatsächliche
|
||||
* Ursache nicht hinter einem generischen Platzhalter verborgen bleibt.
|
||||
*
|
||||
* @param technicalMessage die rohe technische Fehlermeldung; darf {@code null} sein
|
||||
* @return eine nicht-leere deutsche Benutzerfehlermeldung ohne führendes Warnsymbol
|
||||
@@ -75,7 +77,19 @@ public final class AiFailureMessageTranslator {
|
||||
return "KI-Dienst nicht erreichbar. Bitte Verbindung und Konfiguration prüfen.";
|
||||
}
|
||||
|
||||
return "Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.";
|
||||
// KI-Antwort strukturell nicht verwertbar (kein/ungültiges JSON, Zusatztext, Pflichtfeld fehlt)
|
||||
if (lower.contains("not_json_object") || lower.contains("not a pure json")
|
||||
|| lower.contains("invalid_json") || lower.contains("could not be parsed")
|
||||
|| lower.contains("empty_response")
|
||||
|| lower.contains("missing_title") || lower.contains("blank_title")
|
||||
|| lower.contains("missing_reasoning")) {
|
||||
return "KI-Antwort war nicht im erwarteten Format (kein gültiges JSON). "
|
||||
+ "Bitte erneut verarbeiten; hält der Fehler an, den Prompt anpassen.";
|
||||
}
|
||||
|
||||
// Unbekannter technischer Fehler: die echte technische Meldung sichtbar machen,
|
||||
// statt sie hinter einem generischen Platzhalter zu verbergen.
|
||||
return "Verarbeitung fehlgeschlagen. Technischer Grund: " + technicalMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+48
-2
@@ -63,7 +63,8 @@ import javafx.scene.layout.VBox;
|
||||
* <p>
|
||||
* Zeigt alle jemals verarbeiteten Dokumente aus der SQLite-Datenbank in einer
|
||||
* zweispaltigen Ansicht: links eine filterbare Dokumentenliste (~55%),
|
||||
* rechts ein Detailbereich mit Stammsatz, Versuchstabelle und KI-Begründung (~45%).
|
||||
* rechts ein Detailbereich mit Stammsatz, Versuchstabelle, KI-Begründung und
|
||||
* KI-Rohantwort (~45%).
|
||||
* Das Suchfeld ist mit einem 300-ms-Debounce ausgestattet (Live-Filter).
|
||||
*
|
||||
* <h2>Layout</h2>
|
||||
@@ -101,6 +102,7 @@ public final class GuiHistoryTab {
|
||||
"Weitere Einträge vorhanden – Filter verwenden um die Trefferliste einzuschränken.";
|
||||
private static final String DETAIL_PLACEHOLDER = "Dokument auswählen für Details";
|
||||
private static final String NO_REASONING_TEXT = "Kein KI-Reasoning für diesen Versuch vorhanden.";
|
||||
private static final String NO_RAW_RESPONSE_TEXT = "Keine KI-Rohantwort für diesen Versuch gespeichert.";
|
||||
private static final String LOADING_TEXT = "Wird geladen …";
|
||||
private static final String LAUF_AKTIV_HINWEIS = "Aktion während Verarbeitungslauf nicht möglich.";
|
||||
|
||||
@@ -142,6 +144,7 @@ public final class GuiHistoryTab {
|
||||
private final ObservableList<ProcessingAttempt> attemptsItems = FXCollections.observableArrayList();
|
||||
private final TextArea failureArea = new TextArea();
|
||||
private final TextArea reasoningArea = new TextArea();
|
||||
private final TextArea rawResponseArea = new TextArea();
|
||||
|
||||
private final Button resetButton = new Button("Status zurücksetzen");
|
||||
private final Button deleteButton = new Button("Eintrag löschen");
|
||||
@@ -452,11 +455,23 @@ public final class GuiHistoryTab {
|
||||
Label reasoningTitle = new Label("KI-Begründung (ausgewählter Versuch)");
|
||||
reasoningTitle.setStyle(BOLD_STYLE);
|
||||
|
||||
// KI-Rohantwort (vollständige, unveränderte Antwort der KI für den Versuch)
|
||||
rawResponseArea.setEditable(false);
|
||||
rawResponseArea.setWrapText(true);
|
||||
rawResponseArea.setPrefRowCount(6);
|
||||
rawResponseArea.setPromptText(NO_RAW_RESPONSE_TEXT);
|
||||
rawResponseArea.setTooltip(new Tooltip(
|
||||
"Die vollständige, unveränderte Antwort der KI für diesen Versuch – "
|
||||
+ "inklusive eventuellem Text um das JSON herum. Nützlich zur Fehlersuche."));
|
||||
Label rawResponseTitle = new Label("KI-Rohantwort (ausgewählter Versuch)");
|
||||
rawResponseTitle.setStyle(BOLD_STYLE);
|
||||
|
||||
VBox rightPane = new VBox(8,
|
||||
detailTitle, detailGrid,
|
||||
attemptsTitle, attemptsTable,
|
||||
failureTitle, failureArea,
|
||||
reasoningTitle, reasoningArea);
|
||||
reasoningTitle, reasoningArea,
|
||||
rawResponseTitle, rawResponseArea);
|
||||
rightPane.setPadding(new Insets(4, 8, 4, 4));
|
||||
VBox.setVgrow(attemptsTable, Priority.ALWAYS);
|
||||
|
||||
@@ -913,15 +928,19 @@ public final class GuiHistoryTab {
|
||||
ProcessingAttempt last = attempts.get(attempts.size() - 1);
|
||||
attemptsTable.getSelectionModel().select(last);
|
||||
showReasoning(last);
|
||||
showRawResponse(last);
|
||||
} else {
|
||||
reasoningArea.setText("");
|
||||
reasoningArea.setPromptText(NO_REASONING_TEXT);
|
||||
rawResponseArea.setText("");
|
||||
rawResponseArea.setPromptText(NO_RAW_RESPONSE_TEXT);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAttemptSelected(ProcessingAttempt attempt) {
|
||||
if (attempt != null) {
|
||||
showReasoning(attempt);
|
||||
showRawResponse(attempt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,6 +973,11 @@ public final class GuiHistoryTab {
|
||||
failureArea.setText(failureMessage != null
|
||||
? AiFailureMessageTranslator.translate(failureMessage) : "");
|
||||
failureArea.setPromptText(NO_ERROR_DETAILS_MSG);
|
||||
// Die technische Originalmeldung als Tooltip bereitstellen, damit die exakte
|
||||
// Ursache jederzeit einsehbar ist, ohne den lesbaren Anzeigetext zu überladen.
|
||||
failureArea.setTooltip(new Tooltip(failureMessage != null
|
||||
? failureMessage
|
||||
: GuiTooltipTexts.VERLAUF_FAILURE_AREA));
|
||||
}
|
||||
|
||||
private void showReasoning(ProcessingAttempt attempt) {
|
||||
@@ -967,12 +991,34 @@ public final class GuiHistoryTab {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt die vollständige KI-Rohantwort des ausgewählten Versuchs an.
|
||||
* <p>
|
||||
* Enthält der Versuch keine gespeicherte Rohantwort (z. B. Skip-/Pre-Check-Versuche
|
||||
* oder Netzwerkfehler vor der Antwort), wird ein Platzhaltertext angezeigt.
|
||||
*
|
||||
* @param attempt der ausgewählte Verarbeitungsversuch; darf nicht {@code null} sein
|
||||
*/
|
||||
private void showRawResponse(ProcessingAttempt attempt) {
|
||||
String raw = attempt.aiRawResponse();
|
||||
if (raw != null && !raw.isBlank()) {
|
||||
rawResponseArea.setText(raw);
|
||||
rawResponseArea.setPromptText("");
|
||||
} else {
|
||||
rawResponseArea.setText("");
|
||||
rawResponseArea.setPromptText(NO_RAW_RESPONSE_TEXT);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearDetailPane() {
|
||||
clearDetailFields();
|
||||
attemptsItems.clear();
|
||||
failureArea.setText("");
|
||||
failureArea.setPromptText(NO_ERROR_DETAILS_MSG);
|
||||
failureArea.setTooltip(new Tooltip(GuiTooltipTexts.VERLAUF_FAILURE_AREA));
|
||||
reasoningArea.setText(DETAIL_PLACEHOLDER);
|
||||
rawResponseArea.setText("");
|
||||
rawResponseArea.setPromptText(NO_RAW_RESPONSE_TEXT);
|
||||
}
|
||||
|
||||
private void clearDetailFields() {
|
||||
|
||||
+23
-2
@@ -164,9 +164,30 @@ class AiFailureMessageTranslatorTest {
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void unbekannterFehler_liefertFallback() {
|
||||
void unbekannterFehler_haengtTechnischeMeldungAn() {
|
||||
String result = AiFailureMessageTranslator.translate("some completely unknown error text");
|
||||
assertEquals("Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.", result);
|
||||
assertEquals("Verarbeitung fehlgeschlagen. Technischer Grund: some completely unknown error text",
|
||||
result);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// KI-Antwort strukturell nicht verwertbar (JSON/Parse)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void notJsonObject_liefertFormatMeldung() {
|
||||
String result = AiFailureMessageTranslator.translate(
|
||||
"AI response could not be parsed [NOT_JSON_OBJECT]: AI response is not a pure JSON object"
|
||||
+ " (contains extra text or is not an object)");
|
||||
assertTrue(result.contains("nicht im erwarteten Format"), result);
|
||||
assertTrue(result.contains("JSON"), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidJson_liefertFormatMeldung() {
|
||||
String result = AiFailureMessageTranslator.translate(
|
||||
"AI response could not be parsed [INVALID_JSON]: AI response is not valid JSON: ...");
|
||||
assertTrue(result.contains("nicht im erwarteten Format"), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user