AUDIT / src /lib /agent /memorySummarizer.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
4.2 kB
/**
* 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);
}