fix: make dave telegram alerts conversational
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 32s
Build / test-and-image (pull_request) Successful in 2m4s

This commit is contained in:
2026-07-11 10:58:37 +02:00
parent 8de9feda62
commit 1178146d59
7 changed files with 178 additions and 40 deletions

View File

@@ -150,10 +150,10 @@ export class TelegramAlerter {
return chunks;
}
_conversationChunks(text, maxLen = 900) {
_conversationChunks(text, maxLen = 420) {
const cleaned = stripVisibleMarkdown(String(text || '').trim());
if (!cleaned) return [];
const limit = Math.max(300, Math.min(1800, Number(maxLen) || 900));
const limit = Math.max(160, Math.min(900, Number(maxLen) || 420));
const paragraphs = cleaned.split(/\n{2,}/).map(part => part.trim()).filter(Boolean);
const chunks = [];
let current = '';
@@ -163,21 +163,19 @@ export class TelegramAlerter {
};
for (const paragraph of paragraphs) {
if (paragraph.length > limit) {
flush();
for (const sentence of splitSentences(paragraph)) {
if ((current.length + sentence.length + 1) > limit) flush();
if (sentence.length > limit) {
chunks.push(...this._chunkText(sentence, limit).map(part => part.trim()).filter(Boolean));
} else {
current = current ? `${current} ${sentence}` : sentence;
}
const sentences = splitSentences(paragraph);
const shouldSentenceSplit = paragraph.length > Math.floor(limit * 0.65) || sentences.length > 2;
const units = shouldSentenceSplit ? sentences : [paragraph];
for (const unit of units) {
if (unit.length > limit) {
flush();
chunks.push(...this._chunkText(unit, limit).map(part => part.trim()).filter(Boolean));
continue;
}
flush();
continue;
if ((current.length + unit.length + 2) > limit) flush();
current = current ? `${current}\n\n${unit}` : unit;
}
if ((current.length + paragraph.length + 2) > limit) flush();
current = current ? `${current}\n\n${paragraph}` : paragraph;
}
flush();
return chunks;
@@ -195,7 +193,7 @@ export class TelegramAlerter {
* Evaluate delta signals with LLM and send tiered alert if warranted.
* Uses semantic dedup, rate limiting, and a much richer evaluation prompt.
*/
async evaluateAndAlert(llmProvider, delta, memory) {
async evaluateAndAlert(llmProvider, delta, memory, opts = {}) {
if (!this.isConfigured) return false;
if (!delta?.summary?.totalChanges) return false;
if (this._isMuted()) {
@@ -262,9 +260,13 @@ export class TelegramAlerter {
return false;
}
// 4. Format and send tiered alert
const message = this._formatTieredAlert(evaluation, delta, tier);
const sent = await this.sendAlert(message);
// 4. Format and send as short conversational messages.
const messages = this._formatTieredAlertConversation(evaluation, delta, tier, opts);
const sendResult = await this.sendConversation(messages, {
maxChunkChars: opts.maxChunkChars ?? 360,
delayMs: opts.delayMs ?? 450,
});
const sent = Boolean(sendResult?.ok || sendResult === true);
if (sent) {
// Mark signals as alerted with content hashing
@@ -963,6 +965,50 @@ Respond with ONLY valid JSON:
return lines.join('\n');
}
_formatTieredAlertConversation(evaluation, delta, tier, opts = {}) {
const german = normalizeLanguage(opts.language) === 'de';
const tierLabel = german
? ({ FLASH: 'SOFORT', PRIORITY: 'PRIO', ROUTINE: 'HINWEIS' }[tier] || 'HINWEIS')
: ({ FLASH: 'NOW', PRIORITY: 'PRIORITY', ROUTINE: 'NOTE' }[tier] || 'NOTE');
const confidence = String(evaluation.confidence || 'MEDIUM').toLowerCase();
const direction = String(delta?.summary?.direction || 'mixed').toLowerCase();
const headline = compactLine(evaluation.headline || (german ? 'Neue relevante Aenderung erkannt.' : 'New relevant change detected.'));
const reason = compactLine(evaluation.reason || '');
const signals = Array.isArray(evaluation.signals) ? evaluation.signals.map(compactLine).filter(Boolean).slice(0, 2) : [];
const messages = [
`[DAVE // ${tierLabel}]\n${headline}`,
];
if (reason && reason !== headline) {
messages.push(reason);
}
if (tier !== 'ROUTINE' || confidence !== 'low') {
messages.push(german
? `Einordnung: ${confidence}, Richtung ${direction}.`
: `Read: ${confidence} confidence, ${direction} direction.`);
}
if (signals.length) {
messages.push(german
? `Ich sehe vor allem: ${signals.join('; ')}.`
: `Main signals: ${signals.join('; ')}.`);
}
if (evaluation.actionable && evaluation.actionable !== 'Monitor') {
messages.push(german
? `Naechster sinnvoller Schritt: ${compactLine(evaluation.actionable)}`
: `Useful next step: ${compactLine(evaluation.actionable)}`);
} else {
messages.push(german
? 'Soll ich das genauer auseinandernehmen?'
: 'Want me to dig into it?');
}
return messages.filter(Boolean);
}
}
// ─── Helpers ──────────────────────────────────────────────────────────────
@@ -1008,6 +1054,20 @@ function splitSentences(text) {
return normalized.match(/[^.!?\n]+[.!?]+(?:\s+|$)|[^.!?\n]+$/g)?.map(item => item.trim()).filter(Boolean) || [normalized];
}
function normalizeLanguage(value) {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'de' || normalized === 'en' ? normalized : null;
}
function compactLine(value, maxLen = 320) {
const cleaned = stripVisibleMarkdown(String(value || ''))
.replace(/\s+/g, ' ')
.replace(/^[-*]\s*/, '')
.trim();
if (cleaned.length <= maxLen) return cleaned;
return `${cleaned.slice(0, maxLen - 1).trim()}...`;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}