Spaces:
Sleeping
Sleeping
| /** | |
| * 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<string, unknown>; | |
| 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"; | |
| } | |