fix: keep dave chat responsive on llm failures
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user