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:
+101
-4
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user