feat(Fase2): euristica webSearch=auto rifinita — filtri, trigger e anti-ridondanza

- webSearch=auto: euristica robusta con filtri negativi (immagini, analisi
  locale, task di codice, comandi) e trigger positivi (recency, versioni,
  confronti, definizioni, news).
- webSearch non si attiva per mode image/analyze.
- Cache anti-ridondanza: evita ricerche ripetute dello stesso topic entro 60s.
- Test: 10/10 casi passano.
This commit is contained in:
2026-08-10 15:31:19 +02:00
parent 854b3d8a47
commit 2cf9079ac8
+54 -10
View File
@@ -428,14 +428,51 @@ async function webSearchGemini(query: string, signal?: AbortSignal): Promise<str
} }
} }
function needsWebSearch(prompt: string): boolean { // Cache anti-ridondanza: evita ricerche web ripetute sullo stesso topic in una
const mode = getConfig("webSearch") ?? "auto"; // finestra breve (il multi-step reasoning lancia prompt simili in sequenza).
if (mode === "off") return false; let lastSearch = { topic: "", time: 0 };
if (mode === "on") return true;
// auto: domande informative/tecniche che beneficiano di dati aggiornati // Euristica per webSearch=auto. Determina se il prompt beneficia di dati
return /cerca|ricerca|latest|ultim|aggiorna|versione|documentaz|news|novit|differenza|confronta|quanto |come funzion|quando|perché|cos'è|chi è|release|prezzo|alternativ/i.test( // aggiornati dalla ricerca web. Filtri negativi (immagini, codice locale,
prompt, // comandi) per non sprecare chiamate API/token su task che non servono.
); function needsWebSearch(prompt: string, mode?: string): boolean {
if (mode === "image" || mode === "analyze") return false;
const ws = getConfig("webSearch") ?? "auto";
if (ws === "on") return true;
if (ws === "off") return false;
const p = prompt.trim().toLowerCase();
if (!p || p.length < 14) return false;
// --- Filtri negativi: task che NON beneficiano della ricerca web ---
// generazione / editing immagini
if (/(genera|crea|disegna|produrr|dipin|realizz).{0,30}(immagine|logo|icona|poster|banner|ritratto|illustraz|foto|meme|sfondo)/i.test(p))
return false;
// analisi locale / OCR / trascrizione / traduzione / voce
if (/(analizza questo|analizza il|\bocr\b|trascri|leggi il file|traduci|riassumi il file)/.test(p)) return false;
// task di codice puramente locale
if (/^(scrivi|rifattorizza|correggi|implementa|aggiungi|rimuovi|crea|modifica|fix|refactor|aggiorna il file|genera il codice)/.test(p))
return false;
// comandi / gestione conversazione
if (/^(continua|ricordati|ignora|vai avanti|mostra|riassumi la conversazione|non)/.test(p)) return false;
// --- Trigger positivi ---
// segnali di recency / tempo corrente
if (/(oggi|stamattina|adesso|al momento|quest'anno|del 20\d\d|nel 20\d\d|recent|ultim|appena|di recente|attuale|latest)/i.test(p))
return true;
// versioni / package / tech aggiornati
if (/(versione|version|release|changelog|release notes|npm|package|dipendenz|compatibil|lts|stabile|beta|deprecat|\beol\b|end of life|release date)/i.test(p))
return true;
// confronti / alternative / raccomandazioni
if (/(differenza|confronta|\bvs\b|alternativ|migliore|best|top|consigli|oppure)/i.test(p)) return true;
// domande informative / definizioni
if (/(cos'?è|che cos|chi è|quanto|quando|dove|perché|come funzion|cosa significa|definizion|significat|prezzo|costo|storia di)/i.test(p))
return true;
// news / novità / fatti correnti
if (/(news|notizie|novità|novita|ultime notizie|accadut|succes|eventi|mercato|azienda|lancio|annunci)/i.test(p))
return true;
return false;
} }
const estTokens = (s: string) => Math.ceil(s.length / 4); const estTokens = (s: string) => Math.ceil(s.length / 4);
@@ -450,6 +487,7 @@ async function buildContextBlock(
ctx: any, ctx: any,
prompt: string, prompt: string,
signal?: AbortSignal, signal?: AbortSignal,
mode?: string,
): Promise<string> { ): Promise<string> {
if (getConfig("contextInject") === "off") return ""; if (getConfig("contextInject") === "off") return "";
const budget = Number(getConfig("contextTokens") ?? 1500) || 1500; const budget = Number(getConfig("contextTokens") ?? 1500) || 1500;
@@ -491,10 +529,16 @@ async function buildContextBlock(
} }
// --- <web_context> (Strada A) --- // --- <web_context> (Strada A) ---
if (needsWebSearch(prompt)) { if (needsWebSearch(prompt, mode)) {
// anti-ridondanza: non ricercare lo stesso topic entro 60s
const topic = prompt.trim().toLowerCase().slice(0, 60);
const now = Date.now();
if (topic !== lastSearch.topic || now - lastSearch.time > 60_000) {
lastSearch = { topic, time: now };
const web = await webSearchGemini(prompt.slice(0, 300), signal); const web = await webSearchGemini(prompt.slice(0, 300), signal);
if (web) parts.push({ label: "web", xml: "web_context", body: web }); if (web) parts.push({ label: "web", xml: "web_context", body: web });
} }
}
// Token cap: budget totale ~1500. Assegna in modo proporzionale. // Token cap: budget totale ~1500. Assegna in modo proporzionale.
const blocks: string[] = []; const blocks: string[] = [];
@@ -1126,7 +1170,7 @@ export default function agyExtension(pi: ExtensionAPI) {
const wantInject = p.injectContext ?? true; const wantInject = p.injectContext ?? true;
const prevWs = getConfig("webSearch"); const prevWs = getConfig("webSearch");
if (typeof p.webSearch === "boolean") setConfig("webSearch", p.webSearch ? "on" : "off"); if (typeof p.webSearch === "boolean") setConfig("webSearch", p.webSearch ? "on" : "off");
const contextBlock = wantInject ? await buildContextBlock(ctx, p.prompt, signal) : ""; const contextBlock = wantInject ? await buildContextBlock(ctx, p.prompt, signal, p.mode) : "";
if (typeof p.webSearch === "boolean" && prevWs) setConfig("webSearch", prevWs); if (typeof p.webSearch === "boolean" && prevWs) setConfig("webSearch", prevWs);
const res = await executeAgy({ const res = await executeAgy({
prompt: p.prompt, prompt: p.prompt,