feat(VocabService): enhance logging and error handling in lesson assistant message flow
All checks were successful
Deploy to production / deploy (push) Successful in 2m18s

- Introduced detailed logging throughout the sendLessonAssistantMessage method to track request lifecycle, including start, abort conditions, and response handling.
- Improved error handling for various scenarios such as disabled assistant, unconfigured settings, empty messages, and fetch failures, providing clearer feedback to users.
- Added logging for response parsing and upstream errors to facilitate debugging and improve overall service reliability.
This commit is contained in:
Torsten Schulz (local)
2026-05-07 08:58:22 +02:00
parent cfab56f63d
commit 2c453a4a6b

View File

@@ -2808,17 +2808,33 @@ export default class VocabService {
} }
async sendLessonAssistantMessage(hashedUserId, lessonId, payload = {}) { 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 user = await this._getUserByHashedId(hashedUserId);
const lesson = await this.getLesson(hashedUserId, lessonId); const lesson = await this.getLesson(hashedUserId, lessonId);
const config = await this._getUserLlmConfig(user.id); 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) { if (!config.enabled) {
log('aborted: assistant disabled in user settings');
const err = new Error('Der Sprachassistent ist in deinen Einstellungen derzeit deaktiviert.'); const err = new Error('Der Sprachassistent ist in deinen Einstellungen derzeit deaktiviert.');
err.status = 400; err.status = 400;
throw err; throw err;
} }
if (!config.configured) { 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.'); const err = new Error('Der Sprachassistent ist noch nicht eingerichtet. Bitte hinterlege zuerst Modell und API-Zugang in den Einstellungen.');
err.status = 400; err.status = 400;
throw err; throw err;
@@ -2826,6 +2842,7 @@ export default class VocabService {
const message = String(payload?.message || '').trim(); const message = String(payload?.message || '').trim();
if (!message) { if (!message) {
log('aborted: empty message');
const err = new Error('Bitte gib eine Nachricht für den Sprachassistenten ein.'); const err = new Error('Bitte gib eine Nachricht für den Sprachassistenten ein.');
err.status = 400; err.status = 400;
throw err; throw err;
@@ -2848,7 +2865,10 @@ export default class VocabService {
const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout >= 30000 const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout >= 30000
? configuredTimeout ? configuredTimeout
: 300000; : 300000;
const timeout = setTimeout(() => controller.abort(), timeoutMs); const timeout = setTimeout(() => {
log('timeout reached, aborting fetch', { timeoutMs });
controller.abort();
}, timeoutMs);
const temperatureByMode = { const temperatureByMode = {
explain: 0.4, explain: 0.4,
@@ -2857,7 +2877,17 @@ export default class VocabService {
}; };
const temperature = Number.isFinite(temperatureByMode[mode]) ? temperatureByMode[mode] : 0.5; 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 response;
let fetchStartedAt = Date.now();
try { try {
response = await fetch(endpoint, { response = await fetch(endpoint, {
method: 'POST', method: 'POST',
@@ -2879,7 +2909,17 @@ export default class VocabService {
] ]
}) })
}); });
log('response received', {
status: response.status,
latencyMs: Date.now() - fetchStartedAt
});
} catch (error) { } 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( const err = new Error(
error?.name === 'AbortError' error?.name === 'AbortError'
? 'Der Sprachassistent hat das Antwort-Zeitlimit überschritten.' ? 'Der Sprachassistent hat das Antwort-Zeitlimit überschritten.'
@@ -2894,11 +2934,16 @@ export default class VocabService {
let responseData = null; let responseData = null;
try { try {
responseData = await response.json(); responseData = await response.json();
} catch { } catch (parseError) {
log('failed to parse response JSON', { message: parseError?.message });
responseData = null; responseData = null;
} }
if (!response.ok) { 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 messageFromApi = responseData?.error?.message || responseData?.message || 'Der Sprachassistent hat die Anfrage abgelehnt.';
const err = new Error(messageFromApi); const err = new Error(messageFromApi);
err.status = response.status || 502; err.status = response.status || 502;
@@ -2907,11 +2952,20 @@ export default class VocabService {
const reply = this._extractAssistantContent(responseData); const reply = this._extractAssistantContent(responseData);
if (!reply) { 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.'); const err = new Error('Der Sprachassistent hat keine verwertbare Antwort geliefert.');
err.status = 502; err.status = 502;
throw err; throw err;
} }
log('done', {
totalMs: Date.now() - startedAt,
model: responseData?.model || config.model,
replyLength: reply.length
});
return { return {
reply, reply,
model: responseData?.model || config.model, model: responseData?.model || config.model,