diff --git a/Changes.md b/Changes.md index 42ba768..9b34645 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,15 @@ --- +## 2026-06-08 – Fix: KI-Verbindungstest mit Diagnoseinformationen (Issue #55) + +- **`LlmService`:** Neue Methode `testConnection()` gibt detaillierte Diagnose zurück (cURL-Fehlernummer/-meldung, HTTP-Code, Antwortzeit, verfügbare Modelle) +- **`LlmService`:** `probeOllama()` schreibt bei cURL-Fehlern jetzt einen Eintrag ins PHP-Fehlerlog (`docker compose logs web`) +- **Einstellungsseite:** Neuer Button „Verbindung testen" im KI-Abschnitt; zeigt bei Erfolg verfügbare Modelle, bei Fehler cURL-Fehler, URL und Firewall-Hinweis an +- **Route:** `POST /settings/llm-test` → `SettingsController::llmTest()` +- **`BelegController`:** Bei Verbindungsfehler wird jetzt `[]` statt `null` gespeichert (verhindert endlose Auto-Retry-Schleife); Fehlermeldung differenziert zwischen „Ollama nicht erreichbar" und „kein Vorschlag generiert" +- **Beleg-Detailseite:** Zeigt bei Verbindungsfehler spezifische Meldung „Ollama nicht erreichbar" mit Link zu den Einstellungen statt der generischen Warnung + ## 2026-06-01 – Fix: EN16931-Validierungsfehler in ZUGFeRD-Rechnungen - **[BR-FX-EN-04]** behoben: `ZugferdService` setzt jetzt BT-72 (Liefer-/Leistungsdatum) via `setDocumentSupplyChainEvent()`; das Datum wird aus dem Feld `leistungszeitraum` geparst (Format `DD.MM.YYYY` → direkt; Format `MM.YYYY` → letzter Tag des Monats; sonst Fallback auf Rechnungsdatum) diff --git a/public/index.php b/public/index.php index 1efd2cb..8a9ef53 100644 --- a/public/index.php +++ b/public/index.php @@ -165,6 +165,7 @@ $router->post('/settings/mail-test', fn($p) => (new SettingsController())->mailT $router->post('/settings/logo', fn($p) => (new SettingsController())->uploadLogo($p)); $router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($p)); $router->post('/settings/generate-app-key', fn($p) => (new SettingsController())->generateAppKey($p)); +$router->post('/settings/llm-test', fn($p) => (new SettingsController())->llmTest($p)); $router->get('/settings/bankimport', fn($p) => (new SettingsController())->bankImport($p)); $router->post('/settings/bankimport/suggest', fn($p) => (new SettingsController())->storeSuggestion($p)); $router->post('/settings/bankimport/create', fn($p) => (new SettingsController())->createBankImportFormat($p)); diff --git a/src/Controllers/BelegController.php b/src/Controllers/BelegController.php index b89cf76..4d145f9 100644 --- a/src/Controllers/BelegController.php +++ b/src/Controllers/BelegController.php @@ -378,8 +378,17 @@ class BelegController extends BaseController $svc = new KontierungsvorschlagService(); $vorschlag = $svc->generateForBeleg($beleg); - Beleg::saveKiVorschlag($id, $vorschlag); - echo json_encode($vorschlag ?? ['error' => 'Kein Vorschlag generiert']); + // [] = "verarbeitet, kein Ergebnis"; null würde endlose Auto-Retry-Schleife auslösen + Beleg::saveKiVorschlag($id, $vorschlag ?? []); + if ($vorschlag === null) { + $llm = new \Steuerkram\Services\LlmService(); + echo json_encode($llm->isReachable() + ? ['error' => 'Kein Vorschlag generiert'] + : ['connection_error' => true] + ); + return; + } + echo json_encode($vorschlag); } public function resetVorschlag(array $params): void diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index 40503aa..aec7af0 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -126,6 +126,13 @@ class SettingsController extends BaseController echo json_encode(['key' => bin2hex(random_bytes(32))]); } + public function llmTest(array $params): void + { + header('Content-Type: application/json'); + $result = (new \Steuerkram\Services\LlmService())->testConnection(); + echo json_encode($result); + } + public function bankImport(array $params): void { $formats = BankSource::all('csv'); diff --git a/src/Services/LlmService.php b/src/Services/LlmService.php index f144826..a41cd29 100644 --- a/src/Services/LlmService.php +++ b/src/Services/LlmService.php @@ -134,9 +134,54 @@ class LlmService } } + /** + * Liefert Diagnose-Informationen für den Verbindungstest auf der Einstellungsseite. + * Gibt ein Array zurück: ['ok' => bool, 'url' => string, 'elapsed' => int, 'error'? => string, 'models'? => string[]] + * + * @return array + */ + public function testConnection(): array + { + if ($this->mode === null) { + return ['ok' => false, 'error' => 'Kein LLM-Provider konfiguriert.']; + } + if ($this->mode === 'openai') { + return ['ok' => true, 'url' => $this->url, 'elapsed' => 0, 'info' => 'OpenAI-Modus – kein Verbindungstest möglich.']; + } + + $url = $this->url . '/api/tags'; + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 5, + CURLOPT_CONNECTTIMEOUT => 3, + ]); + $start = microtime(true); + $response = curl_exec($ch); + $elapsed = (int)round((microtime(true) - $start) * 1000); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $errno = curl_errno($ch); + $errstr = curl_error($ch); + curl_close($ch); + + if ($errno !== 0) { + error_log("[Steuerkram] LlmService::testConnection() cURL-Fehler #{$errno} auf {$url}: {$errstr}"); + return ['ok' => false, 'url' => $url, 'elapsed' => $elapsed, 'error' => "cURL #{$errno}: {$errstr}"]; + } + if ($code < 200 || $code >= 300) { + return ['ok' => false, 'url' => $url, 'elapsed' => $elapsed, 'error' => "HTTP {$code}"]; + } + + $data = json_decode($response, true); + $models = array_column($data['models'] ?? [], 'name'); + + return ['ok' => true, 'url' => $url, 'elapsed' => $elapsed, 'models' => $models]; + } + private function probeOllama(string $baseUrl): bool { - $ch = curl_init($baseUrl . '/api/tags'); + $url = $baseUrl . '/api/tags'; + $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, @@ -144,10 +189,16 @@ class LlmService CURLOPT_NOBODY => false, ]); curl_exec($ch); - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $err = curl_errno($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $errno = curl_errno($ch); + $err = curl_error($ch); curl_close($ch); - return $err === 0 && $code >= 200 && $code < 300; + + if ($errno !== 0) { + error_log("[Steuerkram] LlmService::probeOllama() cURL-Fehler #{$errno} auf {$url}: {$err}"); + } + + return $errno === 0 && $code >= 200 && $code < 300; } private function callOllama(string $prompt): string diff --git a/src/Views/belege/detail.php b/src/Views/belege/detail.php index d05ea6c..ef5bb6e 100644 --- a/src/Views/belege/detail.php +++ b/src/Views/belege/detail.php @@ -1101,6 +1101,16 @@ function _showKiBadgeError() { ''; } +function _showKiBadgeConnectionError() { + const neuBtn = _kiAktiv + ? ' ' + : ''; + document.getElementById('ki-badge').innerHTML = + '
Ollama nicht erreichbar.' + neuBtn + + ' Einstellungen / Verbindung testen' + + '
'; +} + // Normalisiert _kiVorschlag zu einem Array (Objekt oder Array aus DB möglich). // Gibt null zurück wenn kein Ergebnis vorhanden. // Gibt leeres Array [] zurück wenn verarbeitet aber kein Ergebnis erzeugt. @@ -1119,6 +1129,7 @@ async function _startKiAnalyse() { try { const r = await fetch('/belege/' + _belegId + '/vorschlag/generate', {method: 'POST'}); const raw = await r.json(); + if (raw && raw.connection_error) { _showKiBadgeConnectionError(); return; } const arr = _normalizeKiArr(raw); if (arr && arr.length > 0) { _applyKiVorschlag(arr[0]); _showKiBadgeDone(arr); } else _showKiBadgeNoResult(); diff --git a/src/Views/settings/form.php b/src/Views/settings/form.php index facb00f..b558032 100644 --- a/src/Views/settings/form.php +++ b/src/Views/settings/form.php @@ -256,7 +256,7 @@ $reachable = $providerLabel !== null ? $llm->isReachable() : false; $ollamaUrlEnv = getenv('OLLAMA_URL') ?: ($_ENV['OLLAMA_URL'] ?? ''); ?> -

+

✓ Provider erreichbar: @@ -269,6 +269,11 @@ — Kein LLM-Provider konfiguriert (Auto-Discovery localhost:11434 fehlgeschlagen)

+
+ + +
+ @@ -477,6 +482,53 @@ + +