fix: keep dave chat responsive on llm failures
Some checks failed
Codex Template Compliance / template-compliance (pull_request) Successful in 52s
Build / test-and-image (pull_request) Failing after 1m31s

This commit is contained in:
2026-07-11 18:27:12 +02:00
parent 74579375b3
commit 429114f272
7 changed files with 194 additions and 20 deletions

View File

@@ -125,7 +125,7 @@ export class DavePresence {
const evidence = result.evidence?.length
? `${german ? 'Belege' : 'Evidence'}:\n${result.evidence.slice(0, 2).map(item => `- ${String(item).slice(0, 120)}`).join('\n')}`
: '';
const messages = [`[DAVE // ${german ? 'AKTIV' : 'ACTIVE'}]\n${result.answer}`, evidence].filter(Boolean);
const messages = [result.answer, evidence].filter(Boolean);
const sent = typeof this.alerter.sendConversation === 'function'
? await this.alerter.sendConversation(messages, { maxChunkChars: 380 })
: await this.alerter.sendMessage(messages.join('\n\n'), { parseMode: null });

View File

@@ -86,13 +86,25 @@ export class TerminalAgent {
].join('\n\n');
for (let step = 0; step < this.maxSteps; step++) {
const response = await this.provider.complete(this._systemPrompt(mode, responseLanguage), working, {
maxTokens: this.maxTokens,
timeout: this.timeoutMs,
});
let response;
try {
response = await this.provider.complete(this._systemPrompt(mode, responseLanguage), working, {
maxTokens: this.maxTokens,
timeout: this.timeoutMs,
});
} catch (error) {
trace.push({ tool: 'llm', status: 'failed', durationMs: 0, rationale: short(error.message || error) });
const result = localFallbackResult(input, context, responseLanguage, trace, error);
this.lastTraceByChat.set(key, trace);
return result;
}
const decision = parseDecision(response?.text);
if (!decision) {
const answer = String(response?.text || '').trim() || 'The agent returned no usable response.';
const answer = String(response?.text || '').trim();
if (!answer) {
working += '\n\nPROTOCOL ERROR: The model returned an empty response. Return the documented JSON final object, or call one allowlisted tool if needed.';
continue;
}
if (looksLikeProtocolPayload(answer)) {
working += '\n\nPROTOCOL ERROR: The previous tool syntax was malformed. Return the documented JSON tool_call or final object only.';
continue;
@@ -146,10 +158,18 @@ export class TerminalAgent {
}
for (let attempt = 0; attempt < 2; attempt++) {
const response = await this.provider.complete(this._finalPrompt(mode, responseLanguage), `${working}\n\nFINALIZATION ATTEMPT ${attempt + 1}: Synthesize the answer from the evidence above.`, {
maxTokens: this.maxTokens,
timeout: this.timeoutMs,
});
let response;
try {
response = await this.provider.complete(this._finalPrompt(mode, responseLanguage), `${working}\n\nFINALIZATION ATTEMPT ${attempt + 1}: Synthesize the answer from the evidence above.`, {
maxTokens: this.maxTokens,
timeout: this.timeoutMs,
});
} catch (error) {
trace.push({ tool: 'llm_final', status: 'failed', durationMs: 0, rationale: short(error.message || error) });
const result = localFallbackResult(input, context, responseLanguage, trace, error);
this.lastTraceByChat.set(key, trace);
return result;
}
const decision = parseDecision(response?.text);
if (decision?.type === 'final') {
const result = finalResult(decision, trace);
@@ -351,6 +371,74 @@ function finalizationFailureMessage(input) {
: 'I could not reliably synthesize the retrieved evidence into an answer. Internal tool data was not exposed. Please retry the question or cautiously increase TELEGRAM_AGENT_MAX_STEPS.';
}
function localFallbackResult(input, context, preferredLanguage, trace, error) {
const german = preferredLanguage === 'de' || looksGerman(input);
const snapshot = parseSnapshot(context);
const reason = classifyFailure(error);
const parts = [];
parts.push(german
? `Ich bekomme gerade keine stabile Modellantwort (${reason}). Ich nutze deshalb den letzten lokalen Sweep statt abzubrechen.`
: `I am not getting a stable model response right now (${reason}), so I am falling back to the last local sweep instead of going silent.`);
const news = Array.isArray(snapshot?.news) ? snapshot.news.map(item => item.title || item.headline).filter(Boolean).slice(0, 2) : [];
const urgent = Array.isArray(snapshot?.urgentOsint) ? snapshot.urgentOsint.filter(Boolean).slice(0, 1) : [];
const degraded = Array.isArray(snapshot?.degradedSources) ? snapshot.degradedSources.map(item => item.name).filter(Boolean).slice(0, 4) : [];
const ideas = Array.isArray(snapshot?.ideas) ? snapshot.ideas.map(item => item.title || item.ticker).filter(Boolean).slice(0, 2) : [];
if (news.length || urgent.length) {
parts.push(german
? `Im letzten Sweep fallen mir auf: ${[...urgent, ...news].join(' | ')}.`
: `From the last sweep, the main items I see are: ${[...urgent, ...news].join(' | ')}.`);
}
if (ideas.length) {
parts.push(german
? `Marktseitig liegen diese Ideen im Snapshot: ${ideas.join(' | ')}.`
: `Market ideas in the snapshot: ${ideas.join(' | ')}.`);
}
if (degraded.length) {
parts.push(german
? `Einschraenkung: ${degraded.join(', ')} liefern gerade nicht sauber.`
: `Caveat: ${degraded.join(', ')} are not clean right now.`);
}
parts.push(german
? 'Steuern kann ich per Chat: Status, Quellen, Briefing, Maerkte, Evidenz/Websuche, Memory, Szenarien und nach deiner Bestaetigung Sweep oder Alerts.'
: 'From chat I can control status, sources, briefings, markets, evidence/web search, memory, scenarios, and after your confirmation sweeps or alerts.');
return {
answer: parts.filter(Boolean).join('\n\n'),
confidence: 'low',
evidence: [],
notify: false,
priority: 'routine',
trace,
degraded: 'llm_fallback',
};
}
function parseSnapshot(context) {
try {
const parsed = JSON.parse(String(context || '{}'));
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
function classifyFailure(error) {
const text = String(error?.message || error || '').toLowerCase();
if (text.includes('timeout') || text.includes('aborted')) return 'Timeout';
if (text.includes('fetch failed')) return 'Netzwerkfehler';
return 'LLM-Fehler';
}
function looksGerman(input) {
return /\b(wie|was|warum|angriff|russland|gefahr|bitte|ist|sind|kann|koennte|könnte|auch|andere|welt|passiert|pruef|prüf)\b/i.test(String(input || ''));
}
function finalResult(decision, trace) {
return {
answer: String(decision.answer || '').trim() || 'No conclusion was produced.',

View File

@@ -611,10 +611,14 @@ export class TelegramAlerter {
});
} catch (err) {
console.error('[Telegram] AI chat error:', err.message);
await this.sendMessage('AI chat failed. Please try again or use /status to check the LLM configuration.', {
await this.sendConversation([
'Dave bekommt gerade keine stabile Antwort vom Modell.',
'Ich bleibe erreichbar. Frag kurz nach Status, Quellen, Briefing, Maerkten, Websuche oder starte einen Sweep nach Bestaetigung.',
], {
chatId: replyChatId,
replyToMessageId: msg.message_id,
parseMode: null,
maxChunkChars: 360,
});
} finally {
stopTyping();
@@ -978,7 +982,7 @@ Respond with ONLY valid JSON:
const signals = Array.isArray(evaluation.signals) ? evaluation.signals.map(compactLine).filter(Boolean).slice(0, 2) : [];
const messages = [
`[DAVE // ${tierLabel}]\n${headline}`,
german ? `Kurzer Hinweis: ${headline}` : `${tierLabel}: ${headline}`,
];
if (reason && reason !== headline) {