Spaces:
Sleeping
Sleeping
| /** | |
| * memorySummarizer.ts — Auto-summarization della memoria agente (S87 — GAP 4) | |
| * | |
| * Quando la memoria supera la soglia (400 entry), le 100 più vecchie vengono | |
| * compresse in un riassunto compatto via Gemini Flash, poi eliminate. | |
| * Il riassunto entra nella memoria come categoria "summary". | |
| * | |
| * Design: | |
| * - Un solo job alla volta (_summarizing flag) | |
| * - Fire-and-forget: non blocca mai il render path | |
| * - Graceful fallback: se Gemini fallisce -> LRU delete classica | |
| * - Safari-safe: fetch standard, no Worker | |
| * - Il riassunto e in italiano (stessa lingua del sistema) | |
| * | |
| * Modello: gemini-2.5-flash-lite (S325: era gemini-2.5-flash, lite = 15 RPM/1000 RPD gratuiti, piu veloce per compressione) | |
| */ | |
| import { getGeminiToken } from "@/lib/providerChain"; | |
| import type { StoredMemoryEntry } from "@/lib/vfsDb"; | |
| import { makeTimedSignal } from "@/lib/agentLoop/networkConstants"; // Loop-15: iOS-safe | |
| export const SUMMARIZE_THRESHOLD = 200; // S325: era 400 — riassume piu spesso, memoria piu compatta | |
| export const SUMMARIZE_BATCH = 100; // quante entry comprimere per volta | |
| // -- Gemini generateContent (non-streaming) ----------------------------------- | |
| const GEMINI_GEN_URL = | |
| "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent"; // S325: era gemini-2.5-flash | |
| interface GeminiResponse { | |
| candidates?: Array<{ | |
| content?: { parts?: Array<{ text?: string }> }; | |
| }>; | |
| } | |
| async function callGemini(prompt: string): Promise<string | null> { | |
| const token = getGeminiToken(); | |
| if (!token) return null; | |
| try { | |
| const res = await fetch(`${GEMINI_GEN_URL}?key=${token}`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| contents: [{ role: "user", parts: [{ text: prompt }] }], | |
| generationConfig: { temperature: 0.1, maxOutputTokens: 900 }, | |
| }), | |
| signal: makeTimedSignal(15_000), | |
| }); | |
| if (!res.ok) return null; | |
| const data = await res.json() as GeminiResponse; | |
| return data.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| // -- Prompt builder ----------------------------------------------------------- | |
| function buildPrompt(entries: StoredMemoryEntry[]): string { | |
| const lines = entries | |
| .map(e => `[${e.category}] ${e.key}: ${e.value.slice(0, 300)}`) | |
| .join("\n"); | |
| return [ | |
| "Sei un assistente che comprime la memoria a lungo termine di un agente AI.", | |
| `Di seguito ci sono ${entries.length} ricordi da condensare in un riassunto compatto.`, | |
| "", | |
| "RICORDI DA COMPRIMERE:", | |
| lines, | |
| "", | |
| "REGOLE:", | |
| "- Scrivi un riassunto strutturato in bullet point (max 25 righe)", | |
| "- Mantieni SOLO le informazioni utili per future conversazioni (preferenze, fatti, nomi, date importanti)", | |
| "- Scarta dati meteo, news, prezzi (gia filtrati per staleness)", | |
| "- Usa italiano conciso", | |
| "- Inizia direttamente con i bullet, senza preambolo", | |
| "", | |
| "RIASSUNTO:", | |
| ].join("\n"); | |
| } | |
| // -- Main export -------------------------------------------------------------- | |
| export interface SummaryResult { | |
| summary: string; | |
| key: string; | |
| replaced: StoredMemoryEntry[]; | |
| } | |
| /** | |
| * Comprime le N entry piu vecchie in un riassunto via Gemini Flash. | |
| * Ritorna null se Gemini non e disponibile o fallisce (il caller fa LRU delete). | |
| */ | |
| export async function summarizeOldEntries( | |
| entries: StoredMemoryEntry[], | |
| batchSize = SUMMARIZE_BATCH, | |
| ): Promise<SummaryResult | null> { | |
| const candidates = entries | |
| .filter(e => e.category !== "summary") | |
| .sort((a, b) => a.updatedAt - b.updatedAt) | |
| .slice(0, batchSize); | |
| if (candidates.length < 10) return null; | |
| const prompt = buildPrompt(candidates); | |
| const summary = await callGemini(prompt); | |
| if (!summary || summary.length < 50) return null; | |
| const key = `summary:${Date.now()}`; | |
| return { summary, key, replaced: candidates }; | |
| } | |
| /** Stima quante token consuma un batch di entry (approssimata, 4 char = 1 token). */ | |
| export function estimateBatchTokens(entries: StoredMemoryEntry[]): number { | |
| return entries.reduce((acc, e) => acc + (e.key.length + e.value.length) / 4, 0); | |
| } | |