feat(VocabService): enhance logging and error handling in lesson assistant message flow
All checks were successful
Deploy to production / deploy (push) Successful in 2m18s
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user