diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js index ce438ef..d7e9aaa 100644 --- a/backend/services/vocabService.js +++ b/backend/services/vocabService.js @@ -2808,17 +2808,33 @@ export default class VocabService { } async sendLessonAssistantMessage(hashedUserId, lessonId, payload = {}) { + const requestId = `assist-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + const log = (...args) => console.log(`[LLM ${requestId}]`, ...args); + const startedAt = Date.now(); + const user = await this._getUserByHashedId(hashedUserId); const lesson = await this.getLesson(hashedUserId, lessonId); const config = await this._getUserLlmConfig(user.id); + log('start', { + userId: user.id, + lessonId, + enabled: config.enabled, + configured: config.configured, + hasKey: config.hasKey, + baseUrl: config.baseUrl || '(default openai)', + model: config.model + }); + if (!config.enabled) { + log('aborted: assistant disabled in user settings'); const err = new Error('Der Sprachassistent ist in deinen Einstellungen derzeit deaktiviert.'); err.status = 400; throw err; } if (!config.configured) { + log('aborted: assistant not configured'); const err = new Error('Der Sprachassistent ist noch nicht eingerichtet. Bitte hinterlege zuerst Modell und API-Zugang in den Einstellungen.'); err.status = 400; throw err; @@ -2826,6 +2842,7 @@ export default class VocabService { const message = String(payload?.message || '').trim(); if (!message) { + log('aborted: empty message'); const err = new Error('Bitte gib eine Nachricht für den Sprachassistenten ein.'); err.status = 400; throw err; @@ -2848,7 +2865,10 @@ export default class VocabService { const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout >= 30000 ? configuredTimeout : 300000; - const timeout = setTimeout(() => controller.abort(), timeoutMs); + const timeout = setTimeout(() => { + log('timeout reached, aborting fetch', { timeoutMs }); + controller.abort(); + }, timeoutMs); const temperatureByMode = { explain: 0.4, @@ -2857,7 +2877,17 @@ export default class VocabService { }; const temperature = Number.isFinite(temperatureByMode[mode]) ? temperatureByMode[mode] : 0.5; + log('request', { + endpoint, + mode, + temperature, + timeoutMs, + historyMessages: history.length, + messagePreview: message.slice(0, 120) + }); + let response; + let fetchStartedAt = Date.now(); try { response = await fetch(endpoint, { method: 'POST', @@ -2879,7 +2909,17 @@ export default class VocabService { ] }) }); + log('response received', { + status: response.status, + latencyMs: Date.now() - fetchStartedAt + }); } catch (error) { + log('fetch failed', { + name: error?.name, + message: error?.message, + cause: error?.cause?.code || error?.cause?.errno || null, + latencyMs: Date.now() - fetchStartedAt + }); const err = new Error( error?.name === 'AbortError' ? 'Der Sprachassistent hat das Antwort-Zeitlimit überschritten.' @@ -2894,11 +2934,16 @@ export default class VocabService { let responseData = null; try { responseData = await response.json(); - } catch { + } catch (parseError) { + log('failed to parse response JSON', { message: parseError?.message }); responseData = null; } if (!response.ok) { + log('upstream returned non-ok', { + status: response.status, + body: responseData ? JSON.stringify(responseData).slice(0, 500) : null + }); const messageFromApi = responseData?.error?.message || responseData?.message || 'Der Sprachassistent hat die Anfrage abgelehnt.'; const err = new Error(messageFromApi); err.status = response.status || 502; @@ -2907,11 +2952,20 @@ export default class VocabService { const reply = this._extractAssistantContent(responseData); if (!reply) { + log('empty reply from upstream', { + responsePreview: responseData ? JSON.stringify(responseData).slice(0, 500) : null + }); const err = new Error('Der Sprachassistent hat keine verwertbare Antwort geliefert.'); err.status = 502; throw err; } + log('done', { + totalMs: Date.now() - startedAt, + model: responseData?.model || config.model, + replyLength: reply.length + }); + return { reply, model: responseData?.model || config.model,