Cursus
Lokale LLM’s zitten op een keerpunt. Met Gemma 4 met z’n lange contextwindow, native multimodale ondersteuning en de toegankelijkheid van Ollama is het nu praktisch haalbaar om een capabele, agentische coding assistant volledig op je eigen machine te draaien.
In deze tutorial laat ik je zien hoe je een coding assistant bouwt met Gemma 4 via Ollama, met een Gradio-interface. De app heeft een split-pane lay-out met links een live code-editor en rechts een agentisch chatpaneel. Je kunt afbeeldingen of codebestanden uploaden als context, tools inschakelen zodat het model code kan uitvoeren en valideren, en een denkmode inschakelen voor lastigere problemen.
Aan het einde van deze tutorial heb je een werkende lokale app die kan:
- Code schrijven, uitleggen en debuggen in 15+ talen
- Python-code uitvoeren in een gesandboxte subprocess en resultaten retourneren
- Afbeeldingen en tekstbestanden accepteren als multimodale context
- Antwoorden in realtime streamen vanuit een lokaal draaiend Gemma 4-model
- Draaien als een agentische loop, tools aanroepen en opvolgen op basis van resultaten
De volledige code voor deze tutorial is hier beschikbaar.
Wat is Gemma 4?
Gemma 4 is de open-weights modelfamilie van Google DeepMind, ontworpen voor zowel lokale deployment als onderzoek. Het bouwt voort op de Gemma-lijn met verbeterde instructieopvolging, langere contextwindows en native multimodale inputafhandeling en is gebouwd op dezelfde onderzoeksinfrastructuur als Gemini 3.

Figuur: Modelprestatie vs grootte (bron: Gemma 4-blog)
Modellen zoals Gemma-4-26B(MOE) en Gemma-4-31B behalen Elo-scores die vergelijkbaar zijn met veel grotere modellen, wat wijst op sterke performance-per-parameter. Het 31B-model staat momenteel als 3e open model ter wereld op de Arena AI text leaderboard, en het 26B-model bezet de 6e plek. Dit maakt Gemma 4 bijzonder geschikt voor lokale en resource-beperkte deployments zonder capaciteitsverlies.
De Gemma 4-modelfamilie
Gemma 4 is uitgebracht in vier veelzijdige groottes, waaronder Effective 2B (E2B), Effective 4B (E4B), 26B Mixture of Experts (MoE) en 31B Dense-modellen. De familie valt uiteen in twee duidelijke tiers op basis van het deploymentdoel:
|
Model |
Architectuur |
Totaal aantal params |
Actieve/Effectieve params |
Contextlengte |
Modaliteiten |
|
Gemma-4-31B |
Dense Transformer |
31B |
31B |
256K tokens |
Tekst, Visie, Video |
|
Gemma-4-26B-A4B |
MoE (128 experts) |
26B |
3,8B actief |
256K tokens |
Tekst, Visie, Video |
|
Gemma-4-E4B |
Dense Transformer |
7,9B (met embeddings) |
4,5B effectief |
128K tokens |
Tekst, Audio, Visie, Video |
|
Gemma-4-E2B |
Dense Transformer |
5,1B (met embeddings) |
2,3B effectief |
128K tokens |
Tekst, Audio, Visie, Video |
Laten we dieper ingaan op elk variant:

Figuur: Visuele gids voor Gemma 4 (bron)
- 31B Dense is het vlaggenschipmodel, geoptimaliseerd voor datacenter-deployment en complexe redeneertaken. Het ondersteunt een contextwindow van 256K tokens met een sliding window van 1024 tokens voor efficiënte long-contextverwerking.
- 26B-A4B (MoE) is Gemma’s eerste MoE-model dat tokens routeert via 128 experts, terwijl slechts 3,8B parameters actief zijn per forward pass. Dit levert kwaliteit die bijna gelijk is aan 31B tegen een fractie van de rekenkosten per token, waardoor het goed geschikt is voor high-throughput serving.
- E4B en E2B vormen de on-device en mobile tier. In tegenstelling tot de grotere varianten ondersteunen ze native audio-input voor spraakherkenning naast visie en video, waardoor het de meest multimodaal capabele modellen in de familie zijn voor edge-deployment.
Alle vier de modellen zijn beschikbaar onder de Apache 2.0-licentie en kunnen lokaal worden gedeployed via Ollama, vLLM, llama.cpp of Unsloth.
Voor codeertaken blinkt Gemma 4 uit in:
- Volledige, gestructureerde code schrijven met uitleg
- Redeneren over bestaande codebases wanneer die als context worden aangeleverd
- Tool-use en agentische workflows in combinatie met een orkestratielaag
- Afbeeldingen verwerken naast code (bijv. een UI-screenshot lezen en bijpassende HTML genereren)
In deze tutorial gebruiken we de gemma4:e4b (9,6GB)-variant via Ollama, een gequantiseerde versie die goed geschikt is voor lokale inferentie op consumentenhardware.
Gemma 4 draaien via Ollama
Ollama verzorgt het downloaden, quantization, serving van modellen en biedt een OpenAI-compatibele HTTP-API. In deze tutorial fungeert Ollama als de inferentie-backend en communiceert onze app ermee via localhost:11434.
Om het model te pullen en te draaien:
curl -fsSL https://ollama.com/install.sh | sh
ollama pull gemma4:e4b
ollama serve
Bovenstaande commando’s installeren Ollama lokaal met het officiële installatiescript. Vervolgens downloaden we de gemma 4:e4b modelvariant lokaal voor on-device inferentie. Tot slot start ollama serve de Ollama-server, zodat onze app verzoeken naar het model kan sturen.
Zodra de server draait, maakt de Gradio-app er automatisch verbinding mee.
Gemma 4-demo: bouw een coding assistant met Ollama
In dit onderdeel bouwen we stap voor stap de code-assistent. Op hoofdlijnen doet de app het volgende:
-
Accepteert een bericht in natuurlijke taal in het chatpaneel, met optionele afbeelding- of bestandsbijlagen
-
Injecteert de huidige code uit de editor als context voor het model
-
Stuurt een streamingverzoek naar Gemma 4 via Ollama’s
/api/chat-endpoint -
Roept optioneel tools aan (code-uitvoering, wiskundige evaluatie) in een agentische loop
-
Pusht uit de respons geëxtraheerde codeblokken naar de live editor
Laten we dit stap voor stap opbouwen.

Stap 1: Installeer dependencies
De app heeft een kleine set libraries nodig voor de UI, afbeeldingsverwerking en HTTP-communicatie. Omdat het model lokaal via Ollama draait, zijn er geen cloud-SDK-dependencies.
pip install gradio requests pillow
In dit project gebruiken we:
-
gradiovoor de split-pane editor en chat-UI -
requestsom te communiceren met de Ollama HTTP-API -
pillowvoor ondersteuning van afbeeldingsverwerking in de UI
Dit houdt de omgeving lichtgewicht en werkt op elke machine met Ollama, waaronder macOS met Apple Silicon, Linux en Windows via WSL.
Stap 2: Configuratie en constanten
Voordat we logica schrijven, definiëren we de imports, de modelnaam, de Ollama basis-URL, ondersteunde talen en een standaard systeemprompt. Deze constanten bepalen het gedrag van de hele app.
import base64
import json
import math
import os
import re
import subprocess
import tempfile
from pathlib import Path
import gradio as gr
import requests
OLLAMA_BASE = "http://localhost:11434"
MODEL = "gemma4:e4b"
LANGUAGES = [
"python", "javascript", "typescript", "bash", "sql",
"rust", "go", "java", "c", "cpp", "html", "css",
"json", "yaml", "markdown", "plaintext",
]
TEXT_EXTS = {
".py", ".js", ".ts", ".jsx", ".tsx", ".html", ".css",
".json", ".yaml", ".yml", ".toml", ".md", ".txt",
".csv", ".sql", ".sh", ".bash", ".rs", ".go",
".java", ".c", ".cpp", ".h", ".hpp", ".rb", ".php",
}
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp"}
DEFAULT_SYSTEM = """\
You are an expert coding assistant. When you write code:
- Always wrap it in a markdown code block with the language tag
- Write complete, working code — not fragments
- Briefly explain what the code does
You have a code-runner tool. Use it to validate logic when helpful.\
"""
De sets TEXT_EXTS en IMAGE_EXTS sturen twee verschillende bijlagegedragingen aan. Tekst-/codebestanden worden gelezen en als context in de prompt geïnjecteerd, terwijl afbeeldingsbestanden base64-encoded worden en in het images-veld worden verzonden voor de visiecapaciteiten van Gemma 4.
De DEFAULT_SYSTEM-prompt vormt de codegeneratiestijl van het model, met name de vraag om complete codeblokken met taal-tags, omdat de app deze parseert om code naar de editor te pushen.
Stap 3: Agentische tools definiëren
De assistant kan in een agentische modus werken waarin hij tijdens inferentie tools aanroept. We definiëren twee tools in het standaard functie-aanroep-schema dat Ollama ondersteunt:
TOOLS = [
{
"type": "function",
"function": {
"name": "run_code",
"description": (
"Execute Python code in a sandboxed subprocess and return "
"stdout + stderr. Use this to validate, test, or demonstrate code."
),
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Python code to run (max ~50 lines, 5 s timeout).",
}
},
"required": ["code"],
},
},
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression precisely.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Python-compatible math expression, e.g. 'math.sqrt(2) * 100'",
}
},
"required": ["expression"],
},
},
},
]
De bovenstaande code definieert de tool run_code, die Python uitvoert in een tijdelijk bestand via een subprocess met een time-out van 5 seconden, en zowel stdout als stderr vastlegt. Zo kan het model logica valideren, voorbeelden draaien of live output produceren, niet alleen statische tekst genereren.
De tool calculate daarentegen evalueert wiskundige expressies in een beperkte namespace met alleen de math-module en veilige built-ins. Dit dekt gevallen waarin het model precieze numerieke resultaten nodig heeft zonder een volledig Python-proces op te starten.
Stap 4: Toolexecutie
De executielaag koppelt toolnamen aan hun implementaties. Deze wordt aangeroepen tijdens de agentische loop wanneer het model een tool_calls-blok retourneert.
def _run_python(code: str) -> str:
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
tmp = f.name
try:
r = subprocess.run(
["python3", tmp],
capture_output=True, text=True, timeout=5,
)
out = (r.stdout + r.stderr).strip()
return out[:3000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "⏱ Timed out (>5 s)"
except Exception as e:
return f"Error: {e}"
finally:
os.unlink(tmp)
def _calculate(expr: str) -> str:
ns = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")}
ns.update({"abs": abs, "round": round})
try:
return str(eval(expr, {"__builtins__": {}}, ns)) # noqa: S307
except Exception as e:
return f"Error: {e}"
def execute_tool(name: str, args: dict) -> str:
if name == "run_code":
return _run_python(args.get("code", ""))
if name == "calculate":
return _calculate(args.get("expression", ""))
return f"Unknown tool: {name}"
Enkele noemenswaardige veiligheidskeuzes uit de toolexecutiestack:
-
_run_python()schrijft naar een tijdelijk bestand en ruimt dit op, zodat er geen restbestanden achterblijven, zelfs niet bij fouten -
Output wordt afgekapt op 3.000 tekens om te voorkomen dat de context overspoeld wordt met enorme print-dumps
-
De functie
_calculate()gebruikteval()met__builtins__als lege dict, zodat geen toegang is tot Python built-ins buiten de expliciet toegestanemath-functies -
De time-out is een harde 5-secondenlimiet: lang genoeg voor de meeste validatietaken, kort genoeg om runaway-processen te voorkomen
Stap 5: Helperutilities
Voordat we de kernlogica van de chat bouwen, hebben we een paar hulpfuncties nodig die bestandspaden, afbeeldingscodering en code-extractie afhandelen. Deze helpers worden door de hele streamingpipeline heen aangeroepen.
def encode_image(path: str) -> str | None:
if not path:
return None
try:
with open(path, "rb") as f:
return base64.b64encode(f.read()).decode()
except Exception:
return None
def file_as_context(path: str) -> str | None:
if not path:
return None
p = Path(path)
if p.suffix.lower() not in TEXT_EXTS:
return None
try:
content = p.read_text(encoding="utf-8", errors="replace")[:8000]
lang = p.suffix.lstrip(".")
return f"\n\n**Attached file — {p.name}:**\n```{lang}\n{content}\n```"
except Exception:
return None
def resolve_gradio_path(val) -> str | None:
if val is None:
return None
if isinstance(val, Path):
s = str(val)
return s if s.strip() else None
if isinstance(val, str):
s = val.strip()
return s if s else None
if isinstance(val, dict):
p = val.get("path")
if isinstance(p, str) and p.strip():
return p.strip()
nested = val.get("file")
if isinstance(nested, dict):
np = nested.get("path")
if isinstance(np, str) and np.strip():
return np.strip()
return None
name = getattr(val, "name", None)
if isinstance(name, str) and name.strip():
return name.strip()
return None
def is_image_path(path: str | None) -> bool:
return bool(path) and Path(path).suffix.lower() in IMAGE_EXTS
def extract_last_code_block(text: str) -> tuple[str | None, str]:
blocks = re.findall(r"```(\w*)\n(.*?)```", text, re.DOTALL)
if blocks:
lang, code = blocks[-1]
return code.strip(), lang.strip() or "python"
return None, "python"
def ollama_ok() -> bool:
try:
requests.get(f"{OLLAMA_BASE}/", timeout=2)
return True
except Exception:
return False
def _gradio_content_to_text(content) -> str:
if content is None:
return ""
if isinstance(content, str):
return content
if isinstance(content, list):
parts: list[str] = []
for block in content:
if isinstance(block, dict):
if block.get("type") == "text":
parts.append(str(block.get("text", "")))
elif "text" in block:
parts.append(str(block["text"]))
elif isinstance(block, str):
parts.append(block)
return "".join(parts)
return str(content)
def _append_chat_turn(
history: list | None, user_text: str, assistant_text: str
) -> list:
base = list(history) if history else []
return base + [
{"role": "user", "content": user_text},
{"role": "assistant", "content": assistant_text},
]
def run_code_btn(code: str) -> str:
if not code.strip():
return "Nothing to run."
result = _run_python(code)
return result
Deze utilities zorgen voor soepele afhandeling van multimodale input en gestructureerde output:
-
encode_image()-functie: Deze functie converteert een afbeelding naar eenbase64-string zodat deze kan worden doorgegeven aan hetimages-veld van Ollama. Hierdoor kan Gemma afbeeldingen naast tekst verwerken voor multimodaal redeneren. -
file_as_context()-functie: Deze leest tekst-/codebestanden en injecteert ze rechtstreeks in de prompt als een opgemaakte markdown-blok. Dit voorkomt dat je een apart retrievalsysteem nodig hebt, omdat alles inline als context wordt meegegeven. -
resolve_gradio_path()-functie: Deze functie normaliseert verschillende bestandsformaten die Gradio teruggeeft naar een bruikbaar pad. Dit voorkomt edge-case bugs bij bestandsafhandeling. -
is_image_path()-functie: Dit is een snelle check of een bestand een afbeelding is. Zo worden afbeeldingsinputs correct gerouteerd. -
extract_last_code_block()-functie: Vervolgens extraheren we het laatste codeblok uit de respons van het model. -
ollama_ok()-functie: We verifiëren dat de lokale Ollama-server draait voordat we verzoeken sturen. -
_gradio_content_to_text()en_append_chat_turn()-functies: We vlakken Gradio’s gestructureerde berichtformaat af naar platte tekst voor consistente verwerking en we behouden de chathistorie. -
run_code_btn()-functie: Tot slot voeren we de gegenereerde code uit wanneer de gebruiker op de knop Run code klikt in de UI.
Nu onze helpers klaarstaan, kunnen we de kernfunctie chat() implementeren.
Stap 6: Kern-chat streaminggenerator
Deze functie stuurt de hele interactieloop aan, van inputvoorbereiding tot streamingresponses en toolexecutie.
Op hoofdlijnen doet chat() het volgende:
- bouwt de conversatiegeschiedenis op
- verrijkt de huidige gebruikersinput met context (code, bestanden, afbeeldingen)
- stuurt een streamingverzoek naar Ollama
- voert optioneel tools uit (agentische modus)
- levert deelresponses voor realtime UI-updates
def chat(
message, history, image_path, file_path,
editor_code, language, system_prompt,
agentic, thinking, temperature,
):
De functie volgt de volgende stap-voor-stap-sequentie:
Stap 1: Berichtgeschiedenis opbouwen
We beginnen met het converteren van eerdere chatbotbeurten naar het berichtenformaat van Ollama. De systeemprompt wordt vooraan geplaatst om het gedrag van het model te sturen, zodat het model over de volledige conversatiecontext beschikt voordat het een antwoord genereert.
for h in history or []:
if not isinstance(h, dict):
continue
role = h.get("role")
if role not in ("user", "assistant"):
continue
messages.append(
{"role": role, "content": _gradio_content_to_text(h.get("content"))}
)
content = message
Stap 2: De gebruikerbeurt samenstellen
Vervolgens construeren we het huidige gebruikersbericht door extra context in te voegen. Als de gebruiker met code werkt, voegen we de inhoud van de editor toe:
if editor_code.strip() and editor_code.strip() != STARTER_CODE.strip():
content += (
f"\n\n**Current code in editor ({language}):**\n"
f"```{language}\n{editor_code}\n```"
)
Op dezelfde manier worden geüploade bestanden toegevoegd als inline context en worden afbeeldingen base64-gecodeerd en via het images-veld meegegeven. Zo kan het model samen over tekst, code en afbeeldingen redeneren.
Stap 3: Het Ollama-verzoek versturen
De payload bevat de modelnaam, berichtgeschiedenis, streamingvlag en opties als volgt:
payload = {
"model": MODEL,
"messages": messages,
"stream": True,
"options": options,
}
if agentic:
payload["tools"] = TOOLS
Als denkmode is ingeschakeld, wordt options["think"] = True gezet, wat uitgebreide chain-of-thought reasoning activeert in ondersteunde modellen.
Stap 4: Tool-calls afhandelen (agentische modus)
Als de agentische modus is ingeschakeld, kan het model in plaats van tekst tool-calls teruggeven. In dat geval wordt elke tool uitgevoerd en worden de resultaten aan de conversatie toegevoegd. Vervolgens wordt een vervolgaanvraag verzonden om de generatie voort te zetten.
if agentic and msg.get("tool_calls"):
for tc in msg["tool_calls"]:
fn_name = tc["function"]["name"]
fn_args = tc["function"]["arguments"]
result = execute_tool(fn_name, fn_args)
if fn_name == "run_code" and fn_args.get("code"):
new_code = fn_args["code"]
messages.append(msg)
messages.append({"role": "tool", "content": result})
resp2 = requests.post(f"{OLLAMA_BASE}/api/chat", ...)
Dit creëert een agentische loop van één beurt waarin het model een tool aanroept, het resultaat ziet en vervolgens het eindoordeel verder genereert. Tool-call-logs worden voor de gebruiker aan de chat toegevoegd.
Stap 5: Code extraheren en editor updaten
Tot slot, zodra het streamen voltooid is, extraheren we het laatste codeblok uit de respons:
extracted, _ = extract_last_code_block(full_response)
if extracted:
new_code = extracted
Kortom: het model genereert code, de editor wordt automatisch bijgewerkt en handmatig kopiëren-plakken is niet nodig.
Stap 7: Gradio UI-lay-out
Met de kernlogica op zijn plek ontwerpen we nu de gebruikersinterface met Gradio. Het doel is een lay-out te maken die zowel coding-workflows als AI-ondersteunde interactie naast elkaar ondersteunt.
We gebruiken gr.Blocks om een tweekoloms lay-out te definiëren:
- het linkerpaneel richt zich op codebewerking en -uitvoering
- het rechterpaneel verzorgt chat, bijlagen en modelbediening
with gr.Blocks(title="Gemma 4 · Code Assistant") as demo:
with gr.Row(equal_height=False):
# LEFT: Code Editor
with gr.Column(scale=11):
with gr.Row():
lang_sel = gr.Dropdown(choices=LANGUAGES, value="python", label="Language")
run_btn = gr.Button("Run Code", elem_classes=["run-btn"])
clear_ed = gr.Button("Clear")
code_editor = gr.Code(
value=STARTER_CODE,
language="python",
label="Editor",
lines=24,
interactive=True,
)
run_output = gr.Textbox(
label="Output",
lines=6,
interactive=False,
elem_id="run-output",
)
# RIGHT: Chat
with gr.Column(scale=9):
chatbot = gr.Chatbot(value=[], elem_id="chatbot", height=430)
with gr.Row():
image_upload = gr.Image(label="Image (vision)", type="filepath")
file_upload = gr.File(label="Code / text file")
with gr.Row():
msg_input = gr.Textbox(placeholder="Ask the agent...", scale=6)
send_btn = gr.Button("Send", variant="primary")
with gr.Row():
agentic_cb = gr.Checkbox(label="Enable Agentic", value=True)
thinking_cb = gr.Checkbox(label="Enable Thinking", value=False)
clear_chat = gr.Button("Clear chat")
with gr.Accordion("Settings", open=False):
sys_prompt = gr.Textbox(value=DEFAULT_SYSTEM, label="System prompt")
temperature = gr.Slider(minimum=0.0, maximum=2.0, value=0.7)
Laten we begrijpen hoe de UI samenwerkt met alle hierboven beschreven componenten:
- Codewerkruimte: Het linkerpaneel bevat een taalselector, een code-editor en knoppen voor uitvoeren/leegmaken. De uitvoer wordt getoond in een gemarkeerd tekstvak.
- Chat en interactie: Het rechterpaneel bevat de chatbot voor gesprekken, een invoerveld voor vragen en ondersteuning voor bijlagen zoals afbeeldingen (voor multimodaal redeneren) en bestanden (voor contextinjectie). Extra toggles schakelen agentische tool-use en optionele denkmodes in.
- Lay-out en gebruiksgemak: De interface gebruikt een tweekoloms lay-out met een 55/45-verdeling, waarbij de code-editor prioriteit krijgt terwijl de chat makkelijk toegankelijk blijft. Deze balans zorgt voor een naadloze workflow tussen coderen, AI-assistentie en uitvoering.
Stap 8: Event-wiring
De verzend- en submit-events delen dezelfde inputs en outputs, dus zowel het klikken op de knop als het indrukken van Enter in het tekstvak triggert identiek gedrag:
_inputs = [
msg_input, chatbot, image_upload, file_upload,
code_editor, lang_sel,
sys_prompt, agentic_cb, thinking_cb, temperature,
]
_outputs = [chatbot, code_editor, msg_input]
send_btn.click(fn=respond, inputs=_inputs, outputs=_outputs)
msg_input.submit(fn=respond, inputs=_inputs, outputs=_outputs)
De wrapper respond() zet hier Gradio-bestandspadobjecten om naar gewone padstrings voordat ze aan de streaminggenerator worden doorgegeven. Vervolgens geeft hij bij elke chunk (history, code, "") terug zodat de lege string het berichtveld bij de eerste yield wist.
Stap 9: Theming en CSS
Om de bruikbaarheid en visuele consistentie te verbeteren, voegen we een custom theme en CSS-laag toe bovenop de standaardstyling van Gradio. Dit geeft de app een verzorgde, GitHub-achtige look met ondersteuning voor zowel light- als dark-mode. Dit is volledig optioneel.
We beginnen met een basisthema met gr.themes.Base:
THEME = gr.themes.Base(
primary_hue = "blue",
secondary_hue = "slate",
neutral_hue = "slate",
font = [gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
font_mono = [gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
)
Dit zet een consistente kleurenpalet en strakke UI-typografie (Inter) met een ontwikkelaarsvriendelijk monospace-lettertype (JetBrains Mono). Vervolgens overschrijven we belangrijke CSS-variabelen om het uiterlijk van de app te sturen:
gradio-app {
--body-background-fill: #f0f3f7;
--block-background-fill: #ffffff;
--body-text-color: #1f2328;
--button-primary-background-fill: #1a7f37;
}
body.dark gradio-app {
--body-background-fill: #0d1117;
--block-background-fill: #1c2128;
--body-text-color: #e6edf3;
--button-primary-background-fill: #238636;
}
Hier definieert het eerste blok het lichtthema, terwijl het tweede blok waarden overschrijft voor dark-mode. Hoewel deze stap optioneel is, verbeteren custom theming en CSS de algehele gebruikerservaring aanzienlijk.
Nu de UI volledig is gestructureerd en gestyled, zijn we klaar voor de laatste stap, namelijk het starten van de applicatie.
Stap 10: Launch
Tot slot start je de app met:
if __name__ == "__main__":
print(f"Model : {MODEL}")
print(f"Ollama : {OLLAMA_BASE}")
if not ollama_ok():
print("Ollama not detected — run ollama serve before chatting")
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
theme=THEME,
css=CSS,
)
Hier zorgt server_name="0.0.0.0" ervoor dat de app toegankelijk is op je lokale netwerk en met share=False blijft alles lokaal. De check ollama_ok() doet een snelle GET naar localhost:11434 en waarschuwt bij het opstarten als Ollama niet draait, in plaats van de fout pas te tonen wanneer de gebruiker voor het eerst een bericht stuurt.
Navigeer tenslotte naar http://localhost:7860 zodra de app draait.
Hieronder zie je de volledige uitvoerstroom vanuit het perspectief van de gebruiker:

-
De gebruiker typt een bericht in het chatpaneel, eventueel met het uploaden van een afbeelding of codebestand en het toggelen van agentische/denkmodus
-
Bij verzenden bouwt de functie
respond()het volledige Ollama-verzoek op, inclusief de huidige code uit de editor en eventuele bijlagen als context -
Het verzoek streamt token voor token en deelresponses worden doorgegeven aan de Gradio-chatbot voor live renderen
-
Als agentische modus aanstaat en het model besluit een tool aan te roepen, draait de tool lokaal, worden de resultaten aan de conversatie toegevoegd en streamt een vervolgaanvraag het definitieve antwoord
-
Na afronding van het streamen wordt het laatste fenced codeblok in de respons geëxtraheerd en automatisch naar de editor gepusht
Conclusie
In deze tutorial bouwden we een volledig lokale AI coding assistant met Gemma 4, Ollama en Gradio. De app ondersteunt multimodale input, echte tool-use, streamingantwoorden en een live code-editor, allemaal op je eigen machine zonder externe API.
De whole-context aanpak waarbij je de code uit de editor direct in elke prompt injecteert, is eenvoudiger dan RAG voor een single-file workflow en werkt bijzonder goed voor taken als uitleggen, refactoren of uitbreiden van code waar je actief aan werkt.
Vanaf hier kun je het project in verschillende richtingen uitbreiden:
- Voeg een bestandsboom-paneel toe om multi-file projecten te ondersteunen en injecteer geselecteerde bestanden als context
- Voeg ondersteuning toe voor extra Ollama-modellen via een dropdown, zodat gebruikers kunnen wisselen tussen Gemma 4, Llama 3 en andere
- Bewaar conversatiegeschiedenis op schijf zodat sessies een app-herstart overleven
Ik ben een Google Developers Expert in ML (Gen AI), een Kaggle 3x Expert en een Women Techmakers Ambassador met meer dan 3 jaar ervaring in tech. In 2020 heb ik een healthtech-startup mee opgericht en ik volg een master computer science aan Georgia Tech, met als specialisatie machine learning.
Gemma 4 FAQ’s
Heb je hier een GPU voor nodig?
Niet per se. Gemma 4 e4b is een gequantiseerd model dat op de CPU draait, al zal een GPU de inferentiesnelheid aanzienlijk verbeteren.
Wat is het verschil tussen agentische modus en gewone chat?
In de normale modus streamt het model alleen tekst. In de agentische modus kan het model run_code of calculate aanroepen, de resultaten controleren en die verwerken voordat het zijn antwoord afrondt.
Hoe werkt het dat de editor automatisch wordt bijgewerkt als de agent code schrijft?
De helperfunctie extract_last_code_block() scant het antwoord van de assistant op het laatste fenced codeblok en pusht dit na het streamen naar de editor. Daarom instrueert de systeemprompt het model om code altijd in fenced blokken met een taaltag te plaatsen.
Wat gebeurt er als Ollama niet draait?
De check ollama_ok() onderschept dit bij het opstarten en bij elke chatsubmissie. Als Ollama niet bereikbaar is, retourneert de chat een opgemaakte foutmelding in plaats van te crashen.

