Gemini API: multi-turn conversation and dynamic system prompt
How to queue messages in the contents array to simulate a conversation with Gemini API,
and build a system prompt that serializes live Firebase data on every call without extra queries.
Basic call
A single call to Gemini requires a system_instruction and a contents array
with the user's message.
async function askGemini(userMessage, systemPrompt) { const GEMINI_KEY = '[GEMINI_API_KEY]'; const MODEL = 'gemini-2.5-flash-lite'; const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${GEMINI_KEY}`; const response = await fetch(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ system_instruction: { parts: [{ text: systemPrompt }] }, contents: [{ parts: [{ text: userMessage }] }] }) }); const data = await response.json(); if (!response.ok) throw new Error(data?.error?.message || 'Errore API'); return data.candidates[0].content.parts[0].text; }
Warning: never put the API key directly in the frontend of a public app. For an internal panel with restricted access it's an acceptable tradeoff; alternatively, store it as a Netlify environment variable and call it via a Netlify Function proxy.
Dynamic system prompt from Firebase state
The key is building the system prompt on the fly, serializing the current state already held in memory (synced from Firebase), without extra queries on every message.
function buildSystemPrompt() { const mese = getCurrentMonthLabel(); // es. "Giugno 2026" const utente = sessionStorage.getItem('panelUser') || 'Sconosciuto'; // Classifica operatori con totali per categoria — già in memoria, no query const rankingText = Object.entries(state.operatori) .map(([nome, dati]) => `${nome}: ${dati.totale} ordini (Cat-A: ${dati.catA}, Cat-B: ${dati.catB})` ).join('\n'); return `Sei un assistente interno. Rispondi sempre in italiano, in modo diretto e professionale. === CONTESTO ATTUALE === Utente connesso: ${utente} Mese di riferimento: ${mese} === OPERATORI E RISULTATI === ${rankingText} === CONOSCENZA AZIENDALE === [Blocco statico — procedure, prodotti, glossario] `; }
Static vs live knowledge: the static block holds things that change rarely (product structure, core procedures, glossary) — written directly as a string in the source. The dynamic block holds everything that changes daily (who sold what, this month's leads), read from Firebase on the fly.
Multi-turn conversation: queuing messages
The Gemini API has no memory of its own. To simulate a multi-exchange conversation, each subsequent call
must include in the contents array all previous messages — alternating
role: "user" and role: "model".
let conversationHistory = []; // resettata all'apertura del pannello async function sendAiMessage(userText) { // Aggiunge il turno utente alla storia conversationHistory.push({ role: 'user', parts: [{ text: userText }] }); const response = await fetch(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ system_instruction: { parts: [{ text: buildSystemPrompt() }] }, contents: conversationHistory // intera storia ad ogni chiamata }) }); const data = await response.json(); const aiText = data.candidates[0].content.parts[0].text; // Aggiunge la risposta del modello alla storia conversationHistory.push({ role: 'model', parts: [{ text: aiText }] }); return aiText; }
Model versioning: Gemini deprecates models quickly for new accounts. Always verify the chosen model's availability before shipping to production — a valid model name today can return a 404 in a few weeks.