/** * ProactiveAgent.ts — Context-aware proactive suggestions (FASE 6) * * Monitors conversation + time + memory to proactively suggest actions. * No LLM required — pure pattern matching + temporal analysis. * * Triggers: * - Time-based: "Good morning" greeting, "notizie del giorno" suggestion at 8am * - Staleness: detects when a prior tool result might be outdated * - Continuity: detects when the user might want to continue a prior task * - Gaps: detects missing context that the agent could fill in advance */ import { agentMemory } from "../agentMemory"; import { sessionMemory } from "./MemoryUpdater"; export interface ProactiveSuggestion { id: string; text: string; reason: string; priority: "high" | "medium" | "low"; tool?: string; args?: Record; expires?: number; // unix ms } // ── Time helpers ────────────────────────────────────────────────────────────── function hourNow(): number { return new Date().getHours(); } function isWeekday(): boolean { return new Date().getDay() >= 1 && new Date().getDay() <= 5; } // ── Pattern matchers ───────────────────────────────────────�����────────────────── export const NEWS_STALE_MS = 4 * 60 * 60 * 1000; // 4h export const WEATHER_STALE_MS = 6 * 60 * 60 * 1000; // 6h // ── Evaluators ──────────────────────────────────────────────────────────────── function checkTimeBased(): ProactiveSuggestion[] { const h = hourNow(); const suggestions: ProactiveSuggestion[] = []; if (h >= 7 && h <= 9) { suggestions.push({ id: "morning-news", text: "Vuoi che ti mostri le notizie del mattino?", reason: "È mattina — potrebbe essere utile un riepilogo", priority: "medium", tool: "get_news", args: { query: "" }, expires: Date.now() + 2 * 60 * 60 * 1000, }); } if (h >= 9 && h <= 10 && isWeekday()) { suggestions.push({ id: "morning-currency", text: "Controlla i tassi di cambio di apertura mercati?", reason: "Apertura mercati finanziari", priority: "low", tool: "get_currency", args: { from: "EUR", to: "USD", amount: 1 }, expires: Date.now() + 3 * 60 * 60 * 1000, }); } if (h >= 18 && h <= 20) { suggestions.push({ id: "evening-summary", text: "Vuoi un riepilogo delle notizie della giornata?", reason: "Orario serale — riepilogo giornaliero", priority: "low", tool: "get_news", args: { query: "oggi" }, expires: Date.now() + 2 * 60 * 60 * 1000, }); } return suggestions; } function checkStaleData(): ProactiveSuggestion[] { const suggestions: ProactiveSuggestion[] = []; const now = Date.now(); // S91: usa agentMemory.list() (Dexie-backed, in-memory cache) invece di localStorage raw. // Evita deserializzazione JSON ad ogni check + accede al layer cache in-memory già caricato. const allMemory = agentMemory.list(); // Check for stale weather const weatherEntry = allMemory.find(e => e.key.startsWith("meteo:") || e.key.startsWith("weather:") || e.category === "weather" || e.category === "meteo" ); if (weatherEntry && now - weatherEntry.updatedAt > WEATHER_STALE_MS) { const val = weatherEntry.value; const cityMatch = val.match(/a\s+(\w+)/i); suggestions.push({ id: "stale-weather", text: `Aggiorna meteo${cityMatch ? ` per ${cityMatch[1]}` : ""}?`, reason: "Dati meteo aggiornati più di 6 ore fa", priority: "low", tool: "get_weather", args: cityMatch ? { city: cityMatch[1] } : {}, expires: now + 60 * 60 * 1000, }); } // Check for stale news const newsEntry = allMemory.find(e => e.key.startsWith("news:") || e.key.startsWith("notizie:") || e.category === "news" || e.category === "notizie" ); if (newsEntry && now - newsEntry.updatedAt > NEWS_STALE_MS) { suggestions.push({ id: "stale-news", text: "Le ultime notizie salvate sono vecchie — aggiorno?", reason: "Notizie aggiornate più di 4 ore fa", priority: "medium", tool: "get_news", args: { query: "" }, expires: now + 30 * 60 * 1000, }); } return suggestions; } function checkContinuity(): ProactiveSuggestion[] { const suggestions: ProactiveSuggestion[] = []; // Look at session memory for interrupted tasks const summary = sessionMemory.summarize(); if (!summary) return suggestions; const hasCode = /run_code|python|javascript/i.test(summary); const hasFile = /write_file|read_file/i.test(summary); const hasResearch = /web_search|wikipedia/i.test(summary); if (hasCode) { suggestions.push({ id: "continue-code", text: "Continuare con il codice della sessione precedente?", reason: "Rilevata sessione di coding in corso", priority: "medium", expires: Date.now() + 10 * 60 * 1000, }); } if (hasFile && !hasCode) { suggestions.push({ id: "continue-file", text: "Mostra i file modificati di recente?", reason: "Rilevata operazione su file", priority: "low", tool: "read_file", expires: Date.now() + 10 * 60 * 1000, }); } if (hasResearch) { suggestions.push({ id: "continue-research", text: "Approfondire la ricerca con Wikipedia?", reason: "Ricerca in corso nella sessione", priority: "low", tool: "search_wikipedia", expires: Date.now() + 15 * 60 * 1000, }); } return suggestions; } // ── Public API ──────────────────────────────────────────────────────────────── let _lastGenerated = 0; const COOLDOWN_MS = 5 * 60 * 1000; // don't spam — max once every 5min /** * Generate proactive suggestions based on current context. * Returns an empty array if called too frequently. */ export function getProactiveSuggestions(maxResults = 3): ProactiveSuggestion[] { const now = Date.now(); if (now - _lastGenerated < COOLDOWN_MS) return []; _lastGenerated = now; try { const all = [ ...checkTimeBased(), ...checkStaleData(), ...checkContinuity(), ]; // Sort by priority, filter expired const valid = all .filter(s => !s.expires || s.expires > now) .sort((a, b) => { const P = { high: 0, medium: 1, low: 2 }; return P[a.priority] - P[b.priority]; }); return valid.slice(0, maxResults); } catch { return []; } } /** * Lightweight version that just checks time (safe to call on every app load). */ export function getTimeBasedGreeting(): string { const h = hourNow(); if (h >= 5 && h < 12) return "Buongiorno"; if (h >= 12 && h < 18) return "Buon pomeriggio"; if (h >= 18 && h < 22) return "Buona sera"; return "Buonanotte"; }