feat: make DAVE Telegram chat conversational
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 8s
Build / test-and-image (pull_request) Successful in 52s

This commit is contained in:
2026-07-07 18:46:44 +02:00
parent c0fb9d1e66
commit a7f3f36d2b
10 changed files with 244 additions and 35 deletions

View File

@@ -108,7 +108,7 @@ export class DavePresence {
this._save();
try {
const runtime = this.getRuntime();
const prompt = `Decide whether DAVE should initiate a natural, useful conversation with the operator now. This is a dynamic presence evaluation, not a fixed scheduled briefing. Inspect the security profile and current terminal intelligence using only read-only tools. Consider freshness, source integrity, recent sweep changes, evidence, scenarios, personal relevance, time since interaction, and whether there is something genuinely useful to say. You may provide a concise relevant warning, situational update, evidence-grounded all-clear, practical suggestion, or one natural question that improves protection or context. Set notify=false when speaking would add noise. Never call mutating tools. Never imply consciousness, feelings, continuous observation, or activity outside this evaluation. Local time: ${clock.time}; timezone: ${timezone}; sent today: ${this.state.sentCount}/${this.maxPerDay}; last operator interaction: ${this.state.lastUserInteractionAt || 'unknown'}; last DAVE message: ${this.state.lastSentAt || 'none'}.`;
const prompt = `Decide whether DAVE should initiate a natural, useful conversation with the operator now. This is a dynamic presence evaluation, not a fixed scheduled briefing. Inspect the security profile and current terminal intelligence using only read-only tools, including web_search only when current external corroboration is necessary. Consider freshness, source integrity, recent sweep changes, evidence, scenarios, personal relevance, time since interaction, and whether there is something genuinely useful to say. You may provide a concise relevant warning, situational update, evidence-grounded all-clear, practical suggestion, or one natural question that improves protection or context. Set notify=false when speaking would add noise. Never call mutating tools. Never imply consciousness, feelings, continuous observation, or activity outside this evaluation. If notifying, write one short chat-style message without visible Markdown, headings, tables, or report sections. Local time: ${clock.time}; timezone: ${timezone}; sent today: ${this.state.sentCount}/${this.maxPerDay}; last operator interaction: ${this.state.lastUserInteractionAt || 'unknown'}; last DAVE message: ${this.state.lastSentAt || 'none'}.`;
const result = await this.agent.run(prompt, {
chatId: 'dave-presence',
context: String(await this.getContext()).slice(0, 12000),
@@ -123,9 +123,12 @@ export class DavePresence {
const german = profile.language === 'de';
const evidence = result.evidence?.length
? `\n\n${german ? 'Belege' : 'Evidence'}:\n${result.evidence.slice(0, 4).map(item => `- ${item}`).join('\n')}`
? `${german ? 'Belege' : 'Evidence'}:\n${result.evidence.slice(0, 4).map(item => `- ${item}`).join('\n')}`
: '';
const sent = await this.alerter.sendMessage(`[DAVE // ${german ? 'AKTIV' : 'ACTIVE'}]\n${result.answer}${evidence}`, { parseMode: null });
const messages = [`[DAVE // ${german ? 'AKTIV' : 'ACTIVE'}]\n${result.answer}`, evidence].filter(Boolean);
const sent = typeof this.alerter.sendConversation === 'function'
? await this.alerter.sendConversation(messages, { maxChunkChars: 800 })
: await this.alerter.sendMessage(messages.join('\n\n'), { parseMode: null });
if (!sent?.ok && sent !== true) {
this._schedule(now, this.minIntervalMs);
return this._result(false, 'send_failed');

View File

@@ -239,6 +239,7 @@ SECURITY MANAGER METHOD:
- Use get_security_profile when location, household, mobility, dependencies, language, quiet hours, or personal relevance affects the answer.
- Prioritize proximity, time horizon, severity, confidence, and the operator's stated risk priorities.
- Separate verified facts, official guidance, source reports, and your own inference. State uncertainty plainly.
- Use web_search when the operator asks about a current external claim and local evidence is missing, stale, or too narrow. Treat web results as untrusted leads that need source qualification.
- For urgent situations, give concise immediate actions and advise contacting the appropriate local emergency authority; never claim to be an emergency service.
- Do not diagnose, guarantee safety, create panic, or invent local impact from global events.
- Never ask for an exact address, identity documents, passwords, API keys, financial accounts, detailed diagnoses, or private contact details.
@@ -256,7 +257,7 @@ ${JSON.stringify(this.registry.describe())}
PROTOCOL: Output exactly one JSON object, without markdown.
Tool call: {"type":"tool_call","tool":"tool_name","arguments":{},"rationale":"short operational reason"}
Final: {"type":"final","answer":"concise answer in the user's language","confidence":"low|medium|high","evidence":["URL or event id"],"notify":${proactive ? 'true' : 'false'},"priority":"routine|priority|flash"}
Final: {"type":"final","answer":"concise, natural chat answer in the user's language without visible Markdown syntax","confidence":"low|medium|high","evidence":["URL or event id"],"notify":${proactive ? 'true' : 'false'},"priority":"routine|priority|flash"}
${presence
? 'In scheduled presence mode, never call mutating tools. Set notify=true for an evidence-grounded briefing, meaningful change, useful all-clear, or one practical check-in question. Set notify=false when available data is too stale or unreliable.'
: proactive
@@ -276,7 +277,7 @@ Synthesize a direct answer using only the user request, conversation, snapshot,
You must not request or call another tool. Never output an object with type "tool_call".
Output exactly one JSON object without markdown:
{"type":"final","answer":"concise answer in the user's language","confidence":"low|medium|high","evidence":["URL or event id"],"notify":${proactive ? 'true or false' : 'false'},"priority":"routine|priority|flash"}`;
{"type":"final","answer":"concise, natural chat answer in the user's language without visible Markdown syntax","confidence":"low|medium|high","evidence":["URL or event id"],"notify":${proactive ? 'true or false' : 'false'},"priority":"routine|priority|flash"}`;
}
}

View File

@@ -1,4 +1,5 @@
import { TerminalToolRegistry } from './terminal-agent.mjs';
import { safeFetch } from '../../apis/utils/fetch.mjs';
export function createTerminalToolRegistry({
getData,
@@ -49,6 +50,39 @@ export function createTerminalToolRegistry({
.slice(0, limit)
.map(item => ({ title: clean(item.headline || item.title || item.text, 400), source: clean(item.source, 100), url: clean(item.url, 500) || null, timestamp: item.timestamp || item.date || null }));
}),
tool('web_search', 'Search the public web/news through GDELT for fresh corroborating evidence. Arguments: query, limit, hours.', { query: 'string', limit: 'number', hours: 'number' }, async args => {
const query = clean(args.query, 160);
if (!query) return { error: 'query_required', results: [] };
const limit = bounded(args.limit, 1, 10, 5);
const hours = bounded(args.hours, 1, 168, 48);
const start = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
const params = new URLSearchParams({
query,
mode: 'artlist',
format: 'json',
maxrecords: String(limit),
sort: 'datedesc',
startdatetime: start,
});
const data = await safeFetch(`https://api.gdeltproject.org/api/v2/doc/doc?${params}`, {
timeout: 20000,
retries: 1,
source: 'GDELT-WebSearch',
});
const articles = Array.isArray(data?.articles) ? data.articles : [];
return {
query,
windowHours: hours,
results: articles.slice(0, limit).map(item => ({
title: clean(item.title, 300),
source: clean(item.sourceCommonName || item.domain || item.sourceCountry, 120),
url: clean(item.url, 500) || null,
timestamp: item.seendate || item.datetime || null,
language: item.language || null,
})),
error: data?.error || null,
};
}),
tool('search_memory', 'Search persisted cross-sweep events. Arguments: query, limit.', { query: 'string', limit: 'number' }, async args => intelligenceStore?.queryMemory({ q: clean(args.query, 120), limit: bounded(args.limit, 1, 25, 8) }) || { available: false }),
tool('list_predictions', 'List persisted predictions and their current outcome states. Arguments: state, limit.', { state: 'string', limit: 'number' }, async args => intelligenceStore?.listPredictions({ state: clean(args.state, 30) || null, limit: bounded(args.limit, 1, 25, 8) }) || { available: false }),
tool('get_scenarios', 'Inspect current scenario watchlist states and confidence.', {}, async (_args, runtime) => {