Spaces:
Sleeping
Sleeping
File size: 4,202 Bytes
cc11e77 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | /**
* 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);
}
|