feat: verifica OpenCV DINAMICA — script generato dall'LLM per ogni requisito

La verifica OpenCV in agy_create_verified non è più hardcodata: l'LLM genera a
runtime uno script Python/OpenCV specifico per i requisiti dell'immagine.
- geminiText: chiamata Gemini generica (solo testo, no tool)
- generateOpenCVScript: genera lo script OpenCV ad hoc dai requisiti
- runOpenCVScript: esegue lo script e restituisce il JSON su stdout
- Contratto JSON: {pass, score, findings, errors}
- Il tool genera lo script UNA volta (i requisiti sono fissi) e lo riusa nel loop
- La decisione di conformità combina visione PASS + OpenCV PASS; se OpenCV rileva
  non conformità, le aggiunge alle issue per guidare la rigenerazione
- Testato: script generato per 'quadrato rosso' ha rilevato correttamente
  forma/4 vertici/simmetria/riempimento/sfondo bianco (pass=true, score=1.0)
This commit is contained in:
2026-08-11 00:30:56 +02:00
parent b2f3cbafb6
commit a81000912a
+101 -4
View File
@@ -467,6 +467,77 @@ async function webSearchGemini(query: string, signal?: AbortSignal): Promise<str
} }
} }
// Chiamata generica alla Gemini API (solo testo, nessun tool). Restituisce la
// risposta completa oppure stringa vuota in caso di errore.
async function geminiText(prompt: string, signal?: AbortSignal): Promise<string> {
let key = getConfig("geminiApiKey") ?? process.env.GEMINI_API_KEY ?? "";
if (!key) {
try {
key = fs.readFileSync(path.join(AGY_CHAT_DIR, "gemini-key"), "utf8").trim();
} catch {
/* ignora */
}
}
if (!key) return "";
const model = process.env.AGY_GEMINI_MODEL ?? "gemini-3.5-flash";
try {
const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(key)}`,
{
method: "POST",
signal,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contents: [{ role: "user", parts: [{ text: prompt }] }],
}),
},
);
if (!res.ok) return "";
const data: any = await res.json();
const text = data?.candidates?.[0]?.content?.parts?.map((p: any) => p.text ?? "").join(" ") ?? "";
return text.trim();
} catch {
return "";
}
}
// Genera DINAMICAMENTE uno script Python/OpenCV specifico per i requisiti,
// usando l'LLM. Lo script legge l'immagine da argv[1] e stampa su stdout un JSON
// standard: {"pass": bool, "score": 0..1, "findings": [...], "errors": [...]}.
async function generateOpenCVScript(requirements: string): Promise<string> {
const prompt =
`Sei un esperto di computer vision con OpenCV. Scrivi UN SINGOLO script Python (solo codice, senza markdown) che verifichi un'immagine rispetto a questi requisiti:\n\n` +
`REQUISITI: ${requirements}\n\n` +
`Lo script:\n` +
`- legge il percorso immagine da sys.argv[1]\n` +
`- importa solo cv2, numpy, json, sys (e eventuali standard library)\n` +
`- esegue verifiche OGGETTIVE pertinenti ai requisiti (es. intervalli di colore HSV, forme/contorni, simmetria, testo, dimensioni, orientamento, ecc.)\n` +
`- stampa su stdout UN SOLO oggetto JSON: {"pass": true/false, "score": 0..1, "findings": [stringhe], "errors": [stringhe]}\n` +
` - pass: true se l'immagine rispetta i requisiti\n` +
` - score: grado di conformità da 0 a 1\n` +
` - findings: brevi note oggettive misurate (es. "colore dominante rosso RGB(200,30,30)")\n` +
` - errors: eventuali problemi di lettura/verifica\n` +
`- NON deve fallire se l'immagine è leggibile: gestisci con try/except e metti l'errore in errors\n` +
`- usa solo OpenCV, numpy e librerie standard (nessuna libreria esterna)\n\n` +
`Restituisci SOLO il codice Python, nessun testo aggiuntivo, nessun blocco markdown.`;
const code = await geminiText(prompt);
// rimuovi eventuali fence markdown
return code.replace(/```python|```/g, "").trim();
}
// Esegue lo script OpenCV generato sull'immagine e restituisce il JSON su stdout.
async function runOpenCVScript(script: string, imagePath: string): Promise<string> {
if (!script) return "";
try {
const { stdout } = await execFileAsync("python3", ["-c", script, imagePath], {
timeout: 20_000,
});
return stdout.trim();
} catch {
return "";
}
}
// Cache anti-ridondanza: evita ricerche web ripetute sullo stesso topic in una // Cache anti-ridondanza: evita ricerche web ripetute sullo stesso topic in una
// finestra breve (il multi-step reasoning lancia prompt simili in sequenza). // finestra breve (il multi-step reasoning lancia prompt simili in sequenza).
let lastSearch = { topic: "", time: 0 }; let lastSearch = { topic: "", time: 0 };
@@ -1692,6 +1763,14 @@ export default function agyExtension(pi: ExtensionAPI) {
}[] = []; }[] = [];
let finalText = ""; let finalText = "";
// Genera DINAMICAMENTE lo script OpenCV specifico per i requisiti (una volta,
// i requisiti sono fissi) e lo riusa in ogni iterazione del loop.
let cvScript = "";
if (useCV) {
toolUpdate(onUpdate, "agy_create_verified: generazione script OpenCV dinamico...");
cvScript = await generateOpenCVScript(requirements);
}
for (let iter = 1; iter <= maxIter; iter++) { for (let iter = 1; iter <= maxIter; iter++) {
toolUpdate( toolUpdate(
onUpdate, onUpdate,
@@ -1734,15 +1813,33 @@ export default function agyExtension(pi: ExtensionAPI) {
signal, signal,
}); });
const verdict = judgeRes.text.trim(); const verdict = judgeRes.text.trim();
const pass = /^\s*PASS:?/i.test(verdict); const visionPass = /^\s*PASS:?/i.test(verdict);
lastIssues = verdict; lastIssues = verdict;
// 3) Metriche OpenCV oggettive (opzionale) // 3) Verifica OpenCV DINAMICA (script generato dall'LLM per i requisiti)
let cvMetrics = ""; let cvMetrics = "";
if (useCV) { let ocvPass: boolean | null = null;
cvMetrics = await runOpenCV(imgPath); if (useCV && cvScript) {
const out = await runOpenCVScript(cvScript, imgPath);
cvMetrics = out;
try {
const parsed = JSON.parse(out);
ocvPass = typeof parsed.pass === "boolean" ? parsed.pass : null;
if (typeof parsed.score === "number") cvMetrics += `\nscore: ${parsed.score}`;
if (Array.isArray(parsed.findings) && parsed.findings.length)
cvMetrics += `\nfindings: ${parsed.findings.join("; ")}`;
} catch {
ocvPass = null;
}
// Se OpenCV rileva non conformità, le aggiunge alle issue per guidare la rigenerazione
if (ocvPass === false) {
lastIssues = `${verdict}\nVerifica OpenCV: ${cvMetrics}`;
}
} }
// Conforme solo se visione PASS e (se disponibile) anche OpenCV PASS
const pass = visionPass && (ocvPass === null || ocvPass === true);
report.push({ iteration: iter, pass, imagePath: imgPath, verdict, opencv: cvMetrics }); report.push({ iteration: iter, pass, imagePath: imgPath, verdict, opencv: cvMetrics });
if (pass) { if (pass) {