feat: pipeline multimodale diretta per F12 con modelli Gemini

This commit is contained in:
Matteo Benedetto
2026-08-21 18:24:34 +02:00
parent c9290e9329
commit 49e6269626
+219 -51
View File
@@ -728,18 +728,20 @@ function stageExternalFiles(
}
// Estrae dal briefing JSON prodotto dall'interpretazione vocale i campi per
// l'orchestratore: prompt pulito, se serve ricerca web e suggerimenti.
// l'orchestratore: trascrizione, prompt pulito, se serve ricerca web e suggerimenti.
function extractVoiceBriefing(text: string): {
transcript: string;
cleanPrompt: string;
needsSearch: boolean;
searchHints: string[];
} {
const result = { cleanPrompt: "", needsSearch: false, searchHints: [] as string[] };
const result = { transcript: "", cleanPrompt: "", needsSearch: false, searchHints: [] as string[] };
try {
const start = text.indexOf("{");
const end = text.lastIndexOf("}");
if (start >= 0 && end > start) {
const obj = JSON.parse(text.slice(start, end + 1));
if (typeof obj.trascrizione_corretta === "string") result.transcript = obj.trascrizione_corretta.trim();
if (typeof obj.prompt_utente_pulito === "string") result.cleanPrompt = obj.prompt_utente_pulito.trim();
result.needsSearch = String(obj.ricerca_necessaria).toLowerCase() === "true";
if (Array.isArray(obj.suggerimenti_ricerca)) {
@@ -1442,6 +1444,10 @@ interface AntgModelInfo {
thinkingLevel?: "low" | "medium" | "high";
}
const ANTG_MODEL_MAP: Record<string, AntgModelInfo> = {
"gemini-3.7-flash": { backend: "gemini-3.7-flash-medium" },
"gemini-3.7-flash-medium": { backend: "gemini-3.7-flash-medium" },
"gemini-3.7-flash-low": { backend: "gemini-3.7-flash-low" },
"gemini-3.7-flash-high": { backend: "gemini-3.7-flash-high" },
"gemini-3.5-flash": { backend: "gemini-3.5-flash-low" },
"gemini-3.6-flash-medium": { backend: "gemini-3.6-flash-medium" },
"gemini-3.6-flash-high": { backend: "gemini-3.6-flash-high" },
@@ -1451,7 +1457,7 @@ const ANTG_MODEL_MAP: Record<string, AntgModelInfo> = {
"claude-opus-4.6": { backend: "claude-opus-4-6-thinking" },
"gpt-oss-120b": { backend: "gpt-oss-120b-medium" },
};
const ANTG_DEFAULT_MODEL = "gemini-3.5-flash";
const ANTG_DEFAULT_MODEL = "gemini-3.7-flash-medium";
let antgToken: { access: string; expiresAtMs: number; refresh: string } | null = null;
let antgProject: { pid: string; base: string } | null = null;
@@ -1583,7 +1589,8 @@ async function antgGetProject(): Promise<{ pid: string; base: string }> {
}
interface AntgGenerateOpts {
prompt: string;
prompt?: string;
parts?: any[];
model?: string;
system?: string;
maxOutputTokens?: number;
@@ -1649,8 +1656,9 @@ async function antgGenerate(opts: AntgGenerateOpts): Promise<{ text: string; det
};
if (mi.thinkingLevel) gc.thinkingConfig = { thinkingLevel: mi.thinkingLevel };
if (opts.thinking && opts.thinking !== "off") gc.thinkingConfig = { thinkingLevel: opts.thinking };
const userParts = opts.parts ?? (opts.prompt ? [{ text: opts.prompt }] : []);
const request: any = {
contents: [{ role: "user", parts: [{ text: opts.prompt }] }],
contents: [{ role: "user", parts: userParts }],
generationConfig: gc,
};
if (opts.system) request.systemInstruction = { parts: [{ text: opts.system }] };
@@ -1710,6 +1718,137 @@ async function antgGenerate(opts: AntgGenerateOpts): Promise<{ text: string; det
return run as Promise<{ text: string; details: any }>;
}
// ---------------------------------------------------------------------------
// Pipeline Multimodale Diretta (Gemini): invio diretto dell'audio senza STT
// ---------------------------------------------------------------------------
function isGeminiModel(model: any): boolean {
if (!model) return true; // se non specificato, default ad Antigravity/Gemini
const id = (model.id || "").toLowerCase();
const provider = (model.provider || "").toLowerCase();
if (id.startsWith("gemini-") || id.includes("gemini")) return true;
if (provider === "google" || provider === "google-vertex") return true;
if (provider === "antigravity") {
if (id.startsWith("claude-") || id.startsWith("gpt-oss")) return false;
return true;
}
return false;
}
interface AudioInterpretationResult {
transcript: string;
cleanPrompt: string;
needsSearch: boolean;
searchHints: string[];
rawText: string;
}
async function interpretAudioDirectGemini(
audioFile: string,
editorText: string,
context: string,
targetModel?: string,
): Promise<AudioInterpretationResult> {
if (!audioHasSpeech(audioFile)) {
throw new Error("Nessun parlato rilevato nell'audio (silenzio o volume troppo basso)");
}
let sendFile = audioFile;
let mimeType = "audio/wav";
if (audioFile.toLowerCase().endsWith(".wav")) {
const ogg = audioFile.replace(/\.wav$/i, ".ogg");
try {
await execFileAsync(
"ffmpeg",
["-hide_banner", "-loglevel", "error", "-y", "-i", audioFile, "-ac", "1", "-ar", "16000", "-c:a", "libopus", "-b:a", "16k", ogg],
{ timeout: 60_000 },
);
sendFile = ogg;
mimeType = "audio/ogg";
} catch {
/* fallback su wav */
}
}
const b64 = fs.readFileSync(sendFile).toString("base64");
const promptInstructions =
`[CONTESTO INTERNO — COMUNICAZIONE TRA AGENTI]\n` +
`Sei un analista tecnico/middleware per un agente AI orchestratore. Ascolta la traccia audio allegata (e l'eventuale testo dell'editor) e produci un briefing strutturato per l'orchestratore. NON rispondere all'utente: il tuo output sarà letto SOLO dall'orchestratore, che poi eseguirà le azioni.` +
(editorText ? `\n\nTesto scritto dall'utente nel campo di input (da combinare con la voce):\n${editorText}` : "") +
`\n\nContesto della conversazione precedente:\n${context || "(nessuno)"}` +
`\n\nRestituisci UN SOLO oggetto JSON (nessun markdown aggiuntivo tranne il blocco json) con questi campi esatti:` +
`\n{"trascrizione_corretta":"...","intent_analisi":"...","note_per_agent":"...","azioni_raccomandate":["..."],"prompt_utente_pulito":"...","ricerca_necessaria":true/false,"suggerimenti_ricerca":["..."]}` +
`\nRegole:` +
`\n- trascrizione_corretta: la trascrizione fedele e completa delle parole pronunciate nell'audio in italiano.` +
`\n- prompt_utente_pulito: la richiesta rielaborata e pulita che l'orchestratore userà come prompt effettivo.` +
`\n- NON eseguire alcuno strumento o azione; solo analisi, trascrizione e briefing in JSON.` +
`\n- ricerca_necessaria=true se serve verificare best practices, documentazione o librerie aggiornate.` +
`\n- suggerimenti_ricerca: termini chiave per la ricerca web se necessaria.`;
const parts = [
{ text: promptInstructions },
{ inlineData: { mimeType, data: b64 } },
];
const model = targetModel && targetModel.startsWith("gemini-") ? targetModel : "gemini-3.7-flash-medium";
try {
const res = await antgGenerate({
parts,
model,
maxOutputTokens: 3000,
thinking: "low",
stream: true,
});
const rawText = res.text.trim();
const briefing = extractVoiceBriefing(rawText);
return {
transcript: briefing.transcript || briefing.cleanPrompt || rawText,
cleanPrompt: briefing.cleanPrompt || rawText,
needsSearch: briefing.needsSearch,
searchHints: briefing.searchHints,
rawText,
};
} catch (err: any) {
// Fallback su Gemini API diretta se Antigravity account token fallisce
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) {
const apiModel = process.env.AGY_GEMINI_MODEL ?? "gemini-3.5-flash";
const body = {
contents: [{ parts: [{ text: promptInstructions }, { inline_data: { mime_type: mimeType, data: b64 } }] }],
};
const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${apiModel}:generateContent?key=${key}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(120_000),
},
);
if (res.ok) {
const data: any = await res.json();
const rawText = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
const briefing = extractVoiceBriefing(rawText);
return {
transcript: briefing.transcript || briefing.cleanPrompt || rawText,
cleanPrompt: briefing.cleanPrompt || rawText,
needsSearch: briefing.needsSearch,
searchHints: briefing.searchHints,
rawText,
};
}
}
throw err;
}
}
// =========================================================================
// Provider "antigravity" — modelli del gateway come provider pi nativo
// Registrato con pi.registerProvider() + streamSimple: appare nel selettore
@@ -2977,56 +3116,85 @@ export default function agyExtension(pi: ExtensionAPI) {
ctx.ui.notify("Registrazione fermata, ottimizzazione audio...", "info");
const optimized = await optimizeAudio(file);
ctx.ui.notify("Trascrizione in corso...", "info");
const tr = await transcribeAudio(optimized);
if (!tr.text) {
ctx.ui.setStatus("agy-rec", "");
ctx.ui.notify(`Trascrizione fallita: ${tr.error ?? "vuota"}`, "error");
playSound("cancel");
return;
}
const transcript = tr.text;
// Legge il testo scritto a mano nel campo editor prima della registrazione
// e lo combina con la trascrizione audio (comportamento "multimodale").
const editorText = (ctx.ui.getEditorText?.() ?? "").trim();
// Interpreta con Gemini (testo) — framing inter-agent: Gemini è un analista
// che scrive un briefing per l'orchestratore (NON risponde all'utente),
// in JSON strutturato, con direttiva plan-first (nessuna esecuzione).
ctx.ui.notify("Interpretazione con Gemini...", "info");
const context = getConversationContext(ctx);
const res = await executeAgy({
prompt:
`[CONTESTO INTERNO — COMUNICAZIONE TRA AGENTI]\n` +
`Sei un analista tecnico/middleware per un agente AI orchestratore. NON rispondere all'utente: il tuo output sarà letto SOLO dall'orchestratore, che poi risponderà all'utente.` +
`\n\nAnalizza la richiesta vocale (e l'eventuale testo dell'editor) e produci un briefing strutturato per l'orchestratore.` +
`\n\nTrascrizione vocale:\n${transcript}` +
(editorText
? `\n\nTesto scritto dall'utente nel campo di input (da combinare con la voce):\n${editorText}`
: "") +
`\n\nContesto della conversazione:\n${context || "(nessuno)"}` +
`\n\nRestituisci UN SOLO oggetto JSON (nessun testo aggiuntivo) con questi campi:` +
`\n{"trascrizione_corretta":"...","intent_analisi":"...","note_per_agent":"...","azioni_raccomandate":["..."],"prompt_utente_pulito":"...","ricerca_necessaria":true/false,"suggerimenti_ricerca":["..."]}` +
`\nRegole:` +
`\n- NON eseguire alcuno strumento o azione; solo analisi e briefing.` +
`\n- Nessun saluto o testo rivolto all'utente: solo JSON tecnico per l'orchestratore.` +
`\n- prompt_utente_pulito = la richiesta rielaborata che l'orchestratore userà come prompt verso l'utente.` +
`\n- ricerca_necessaria=true se serve verificare best practices, versioni, documentazione o dati aggiornati.` +
`\n- suggerimenti_ricerca: se ricerca_necessaria, indica all'orchestratore di usare la ricerca web (Perplexity) e su cosa.`,
stateless: true,
model: "Gemini 3.6 Flash (Medium)",
yolo: true,
});
let transcript = "";
let finalText = "";
let needsSearch = false;
let searchHints: string[] = [];
const activeModel = ctx.model;
const useDirectMultimodal = isGeminiModel(activeModel);
if (useDirectMultimodal) {
// =========================================================================
// Pipeline Multimodale Diretta (Gemini): l'audio viene inviato direttamente
// al modello senza passare per un riconoscitore vocale STT separato.
// =========================================================================
ctx.ui.notify("Interpretazione vocale diretta con Gemini...", "info");
try {
const directRes = await interpretAudioDirectGemini(
optimized,
editorText,
context,
activeModel?.id,
);
transcript = directRes.transcript || directRes.cleanPrompt;
finalText = directRes.cleanPrompt;
needsSearch = directRes.needsSearch;
searchHints = directRes.searchHints;
} catch (err: any) {
ctx.ui.notify(`Elaborazione diretta fallita: ${err.message}. Fallback su trascrizione STT...`, "warning");
}
}
// Fallback o modello non-Gemini: pipeline a 2 passaggi (STT + Briefing)
if (!finalText) {
ctx.ui.notify("Trascrizione in corso...", "info");
const tr = await transcribeAudio(optimized);
if (!tr.text) {
ctx.ui.setStatus("agy-rec", "");
ctx.ui.notify(`Trascrizione fallita: ${tr.error ?? "vuota"}`, "error");
playSound("cancel");
return;
}
transcript = tr.text;
ctx.ui.notify("Interpretazione con Gemini...", "info");
const res = await executeAgy({
prompt:
`[CONTESTO INTERNO — COMUNICAZIONE TRA AGENTI]\n` +
`Sei un analista tecnico/middleware per un agente AI orchestratore. NON rispondere all'utente: il tuo output sarà letto SOLO dall'orchestratore, che poi risponderà all'utente.` +
`\n\nAnalizza la richiesta vocale (e l'eventuale testo dell'editor) e produci un briefing strutturato per l'orchestratore.` +
`\n\nTrascrizione vocale:\n${transcript}` +
(editorText
? `\n\nTesto scritto dall'utente nel campo di input (da combinare con la voce):\n${editorText}`
: "") +
`\n\nContesto della conversazione:\n${context || "(nessuno)"}` +
`\n\nRestituisci UN SOLO oggetto JSON (nessun testo aggiuntivo) con questi campi:` +
`\n{"trascrizione_corretta":"...","intent_analisi":"...","note_per_agent":"...","azioni_raccomandate":["..."],"prompt_utente_pulito":"...","ricerca_necessaria":true/false,"suggerimenti_ricerca":["..."]}` +
`\nRegole:` +
`\n- NON eseguire alcuno strumento o azione; solo analisi e briefing.` +
`\n- Nessun saluto o testo rivolto all'utente: solo JSON tecnico per l'orchestratore.` +
`\n- prompt_utente_pulito = la richiesta rielaborata che l'orchestratore userà come prompt verso l'utente.` +
`\n- ricerca_necessaria=true se serve verificare best practices, versioni, documentazione o dati aggiornati.` +
`\n- suggerimenti_ricerca: se ricerca_necessaria, indica all'orchestratore di usare la ricerca web (Perplexity) e su cosa.`,
stateless: true,
model: "Gemini 3.6 Flash (Medium)",
yolo: true,
});
const briefing = extractVoiceBriefing(res.text);
finalText = briefing.cleanPrompt || res.text.trim() || transcript;
needsSearch = briefing.needsSearch;
searchHints = briefing.searchHints;
}
ctx.ui.setStatus("agy-rec", "");
// Estrae il briefing JSON; fallback sul testo grezzo se non parsato
const briefing = extractVoiceBriefing(res.text);
let finalText = briefing.cleanPrompt || res.text.trim() || transcript;
// Guida per l'agente successivo: usa la ricerca web/Perplexity se necessario
if (briefing.needsSearch) {
const hints = briefing.searchHints.length ? ` Suggerimenti: ${briefing.searchHints.join("; ")}` : "";
if (needsSearch) {
const hints = searchHints.length ? ` Suggerimenti: ${searchHints.join("; ")}` : "";
finalText += `\n\n[Nota per l'agente: per rispondere correttamente, usa la ricerca web (Perplexity) per verificare best practices/versioni/documentazione aggiornate.${hints}]`;
}
@@ -3061,7 +3229,7 @@ export default function agyExtension(pi: ExtensionAPI) {
} else {
pi.sendUserMessage(finalText, { deliverAs: "followUp" });
}
ctx.ui.notify("✅ Trascrizione inviata come prompt a pi", "info");
ctx.ui.notify("✅ Richiesta vocale inviata come prompt a pi", "info");
playSound("done");
// notifica vocale (config ttsNotify o AGY_TTS_NOTIFY=0 per disattivare)