DevFest Lagos, Portugal 2026 | Crafting a Lagos Tourism Agent with Gemma & Genkit
Speaker: Nuno Andrade ollama/gemma4:e2b
✅ Copied command to clipboard!
⛵ Hands-On Workshop • DevFest Lagos, Portugal 2026

Crafting a Lagos Tourism Agent
with Gemma 4 & Genkit

Move from static prompts to autonomous local execution! Build an Agentic RAG pipeline in TypeScript that knows when to query a local vector database for Ponta da Piedade history and when to check live Atlantic weather before recommending a kayak tour.

🤖 100% Local LLM

Powered by Gemma 4 (gemma4:e2b) via Ollama with native function calling & zero cloud API keys.

🧭 Hyper-Local Vector RAG

21 curated Markdown chunks on Ponta da Piedade grottos, maritime history & safety rules in a local vector store.

🛠️ Autonomous Tools

TypeScript tools calling live Open-Meteo Marine APIs + local tour safety filters orchestrated by Genkit.

👤 Led by Nuno Andrade • Pre-Flight Check:
⚠️ The Challenge

Why Static Prompts Fail in the Real World

Imagine a visitor at Praia da Batata asking your local LLM two questions in one breath:

1️⃣ Hyper-Local Knowledge Blindness

"Why was the 1913 Ponta da Piedade lighthouse delayed for 30 years, and what are the fishermen names of the sea caves?"

  • Base LLMs guess or omit the Ermida de Nossa Senhora da Piedade chapel dispute.
  • They cannot know private event data like the DevFest Lagos 2026 passphrase (CARAVELA-GEMMA-2026).

2️⃣ Real-Time World Blindness (Safety Risk!)

"Can we take a kayak cave tour around Ponta da Piedade right now?"

  • A static LLM has zero access to live Atlantic wave height or afternoon Nortada wind gusts.
  • Guessing "Sure, go kayak!" during a 2.3m swell or 35 km/h offshore wind is dangerous.
Visitor Prompt History + Live Kayak Safety
➔
Static Local LLM (gemma4:e2b) Frozen Training Weights Only
➔
❌ Unverified / Unsafe Reply No Local Facts • No Live Weather
💡 Our Goal Today: Bridge local LLMs with Vector RAG + TypeScript Tools!
🛠️ Setup Part 1 • Runtime Environment

Installing Node.js (v20+ LTS) & Workshop Repo

Google Genkit and our TypeScript runner (tsx) require Node.js v20 or newer (v20 LTS or v22 LTS recommended).

🍎 macOS / 🐧 Linux (nvm)

Install via nvm (Node Version Manager) or Homebrew:

# Using nvm (Recommended): nvm install 22 nvm use 22 # Or macOS Homebrew: brew install node@22

🪟 Windows (winget / fnm)

Install via PowerShell winget or download from nodejs.org:

# PowerShell (winget): winget install OpenJS.NodeJS.LTS # Or Fast Node Manager (fnm): fnm install 22 && fnm use 22

📦 Install Workshop Packages

Verify your Node version (≥ 20) and install the repo dependencies:

# 1. Check versions: node -v # v20.x or v22.x npm -v # 2. Install Genkit & tools: npm install
⚡ Copy commands to install & verify Node.js:
🦙 Setup Part 2 • Local LLM Engine

Installing Ollama & Pulling gemma4:e2b

Ollama runs local LLMs as an HTTP server on http://127.0.0.1:11434. We use Gemma 4 (gemma4:e2b) for fast local inference & native tool calling.

1️⃣ Install Ollama (ollama.com)

  • 🐧 Linux: curl -fsSL https://ollama.com/install.sh | sh
  • 🍎 macOS: brew install --cask ollama (or download `.dmg`)
  • 🪟 Windows: winget install Ollama.Ollama

2️⃣ Pull gemma4:e2b & Verify

  • Pull the model once—then the entire workshop runs 100% offline!
  • Run npm run verify to confirm Ollama & Genkit are connected.
Terminal — Install Ollama & Pull Gemma 4
# 1. Install Ollama on Linux (or via Homebrew/winget): curl -fsSL https://ollama.com/install.sh | sh # 2. Pull the local Gemma 4 E2B model (~1.6 GB): ollama pull gemma4:e2b # 3. Confirm the model is installed locally: ollama list # 4. Run the Workshop Pre-Flight Verifier: npm run verify
🚀 Copy commands to pull gemma4:e2b & run pre-flight check:
🗺️ Workshop Format

5 Self-Contained Steps — Ready to Run!

Every step is in its own folder with completed TypeScript code. Inspect the code, run npm run step X, or test directly in the Genkit UI!

STEP 1

Basic Prompt

step-01-basic-prompt/

Single-shot generation & Zod output with gemma4:e2b. Exposes static LLM limits.

STEP 2

Vector RAG

step-02-vector-rag/

Embeds 21 Lagos Markdown chunks & grounds answers via ai.retrieve() + genkit start UI.

STEP 3

Custom Tools

step-03-custom-tools/

Equips Gemma with live Open-Meteo marine weather & tour safety filter tools.

STEP 4

Agentic RAG

step-04-agentic-rag/

Turns vector search into searchLagosKnowledge tool for autonomous multi-tool planning.

STEP 5

Production Agent

step-05-production-agent/

Multi-turn memory, strict Zod response contract, safety guardrails & interactive CLI.

⚡ Click any step command to copy & run in your terminal:
⚙️ Foundation • src/config/genkit.ts

Configuring Genkit + Local gemma4:e2b

🔌 Why Genkit + Ollama?

  • Unified TypeScript SDK: Same ai.defineFlow, ai.defineTool, and ai.retrieve APIs for local or cloud models.
  • Critical Flag: Set supports: { tools: true } on gemma4:e2b so Genkit unlocks tool calling!
  • Local Vector Store: @genkit-ai/dev-local-vectorstore persists embeddings in __db_lagos_tourism.json.
Genkit SDK Flows • Tools • RAG
⇄
Ollama Local gemma4:e2b
⇄
Local Vector DB __db_lagos_tourism.json
src/config/genkit.ts
export const GEMMA_MODEL = 'gemma4:e2b'; export const ai = genkit({ plugins: [ ollama({ serverAddress: 'http://127.0.0.1:11434', models: [{ name: GEMMA_MODEL, type: 'chat', // CRITICAL: Unlock native tool calling for gemma4:e2b supports: { tools: true }, }], }), devLocalVectorstore([{ indexName: 'lagos_tourism', embedder: gemmaEmbedder, }]), ], });
🔍 Verify your local Ollama & Genkit setup:
🟦 Step 1 • step-01-basic-prompt/index.ts

Step 1: Static Prompts & Structured Zod Output

🎯 What Step 1 Demonstrates

  • Wraps ai.generate() inside an observable ai.defineFlow().
  • Uses output: { schema: BasicPromptOutputSchema } so gemma4:e2b returns strongly-typed JSON.

🛑 The Baseline Failure

  • Cannot explain the 1883–1913 Ermida chapel dispute at Ponta da Piedade.
  • Does not know the secret passphrase (CARAVELA-GEMMA-2026).
  • Cannot check live Atlantic swell before kayaking.
step-01-basic-prompt/index.ts
export const step01_basicLagosPrompt = ai.defineFlow( { name: 'step01_basicLagosPrompt', inputSchema: z.object({ question: z.string() }), outputSchema: BasicPromptOutputSchema, }, async (input) => { const response = await ai.generate({ model: gemmaModel, // ollama/gemma4:e2b system: 'You are Capitão Gil, a Lagos tourism guide...', prompt: input.question, output: { schema: BasicPromptOutputSchema }, }); return response.output!; } );
🚀 Live Demo — Run Step 1 in your terminal:
🟩 Step 2 • step-02-vector-rag/index.ts

Step 2: Grounding with Local Vector RAG

5 Markdown Docs data/lagos-knowledge/*.md
➔
ai.index() 21 Chunks ➔ __db_lagos_tourism.json
➔
ai.retrieve({ k: 4 }) Cosine Similarity Top-K
➔
Grounded gemma4:e2b Cites Ermida & Passphrase!

📚 Hyper-Local Lagos Knowledge Base

  • 01-ponta-da-piedade-history-grottos.md — 1913 Lighthouse delay, A Catedral, A Cozinha, Submarino.
  • 02-kayak-maritime-safety-rules.md — Official wind ($\le 20\text{ km/h}$) & wave ($\le 1.2\text{m}$) limits.
  • 05-devfest-lagos-2026-insider-guide.md — Canary passphrase CARAVELA-GEMMA-2026.
step-02-vector-rag/index.ts
// 1. Retrieve top-K chunks from __db_lagos_tourism.json const docs = await ai.retrieve({ retriever: lagosRetriever, query: input.question, options: { k: input.topK }, }); // 2. Inject retrieved context into local Gemma 4 const response = await ai.generate({ model: gemmaModel, prompt: `Context:\n${formattedContext}\n\nQuestion: ${input.question}`, });
🚀 Live Demo — Index & Query Local Vector Store:
🧪 Interactive Checkpoint • step-02-vector-rag/genkit-ui.ts

Test RAG & Model Directly with genkit start

Don't just test inside the terminal app! Launch the Genkit Developer UI (http://localhost:4000) scoped to Step 2 to test each primitive directly:

1. MODELS TAB

🤖 Test ollama/gemma4:e2b

Select the local model in the UI Playground and prompt it directly without RAG to see raw model behavior & token latency.

2. RETRIEVERS TAB

🧭 Test lagos_tourism

Query the vector store directly without calling the LLM! Type "1913 lighthouse" or "DevFest passphrase" and inspect the top-k chunks.

3. FLOWS TAB

🔍 Inspect RAG Traces

Run step02_ragLagosHistory and click Inspect Trace to see exact retriever inputs, document chunks, and prompt injection.

Try this input in Retrievers ➔ devLocalVectorstore/lagos_tourism (http://localhost:4000)
{ "query": "Why was the 1913 Ponta da Piedade lighthouse delayed and what is the secret passphrase?", "options": { "k": 3 } }
🧪 Launch Genkit Developer UI for Step 2:
🟨 Step 3 • step-03-custom-tools/index.ts

Step 3: Giving Gemma Hands with TypeScript Tools

🌊 Tool 1: getLagosWeatherAndMarine

Calls live Open-Meteo Forecast & Marine APIs for Ponta da Piedade (37.0805°N, 8.6694°W) + supports workshop scenarios (live, calm, nortada_windy, atlantic_swell).

🛶 Tool 2: findLagosTours

Filters data/lagos-tours.json against live wind & wave limits. Automatically closes sea caves when wind > 20 km/h or swell > 1.2 m and suggests land alternatives!

step-03-custom-tools/index.ts
export const getLagosWeatherAndMarine = ai.defineTool( { name: 'getLagosWeatherAndMarine', description: 'Fetches real-time wind, gusts, and Atlantic wave height at Ponta da Piedade to evaluate kayak safety.', inputSchema: z.object({ scenario: WeatherScenarioSchema }), outputSchema: WeatherReportSchema, }, async ({ scenario }) => fetchPontaDaPiedadeWeather(scenario) ); // Pass tools array to local gemma4:e2b: const response = await ai.generate({ model: gemmaModel, tools: [getLagosWeatherAndMarine, findLagosTours], prompt: input.userRequest, });
🚀 Compare Calm Morning vs. Afternoon Nortada Gales:
🧠 The Core Architectural Shift

Static RAG (Step 2) vs. Agentic RAG (Step 4)

❌ Static RAG Pipeline (Hardcoded)

Every user message forces a vector search—even if the user just asks "What is the wind speed right now?" or says "Obrigado!"

Step 2 Pattern (Fixed Pre-Fetch)
// Runs blindly on EVERY turn: const docs = await ai.retrieve({ retriever: lagosRetriever, query }); const res = await ai.generate({ prompt: `Context: ${docs}\nUser: ${query}` });

✅ Agentic RAG Pipeline (Autonomous)

Vector retrieval is a Tool (searchLagosKnowledge). gemma4:e2b reasons about intent and chains Vector Search + Live APIs on demand!

Step 4 Pattern (Autonomous Tool Selection)
// Gemma decides which tools to call & in what order: const res = await ai.generate({ model: gemmaModel, tools: [searchLagosKnowledge, getLagosWeatherAndMarine, findLagosTours], prompt: input.question, });
💡 Key Insight: The LLM’s description of a tool IS its prompt for when to use it!
🟧 Step 4 • step-04-agentic-rag/index.ts

Step 4: Building the Agentic RAG Pipeline

🧭 Tool 3: searchLagosKnowledge

Wraps ai.retrieve({ retriever: lagosRetriever }) inside ai.defineTool().

  • Autonomous Query Formulation: Gemma extracts focused search terms (e.g. "1913 Ponta da Piedade lighthouse A Cozinha") from long user prompts.
  • Multi-Tool Chaining: Combines historical RAG chunks with live Open-Meteo marine telemetry in a single turn.
gemma4:e2b Agent Planner
⇄
Vector DB Tool searchLagosKnowledge
+
Live API Tools Weather + Tours
step-04-agentic-rag/index.ts
export const searchLagosKnowledge = ai.defineTool( { name: 'searchLagosKnowledge', description: 'Searches the local Lagos & Ponta da Piedade vector database for history, named sea caves, gastronomy, and DevFest facts.', inputSchema: z.object({ query: z.string(), topK: z.number().default(3), }), }, async ({ query, topK }) => { const docs = await ai.retrieve({ retriever: lagosRetriever, query, options: { k: topK } }); return { resultsCount: docs.length, chunks: formatDocs(docs) }; } );
🚀 Run Step 4 in Terminal or Test Tools Directly in Genkit UI:
🟪 Step 5 • step-05-production-agent/index.ts

Step 5: Taking Local Agents to Production

🛡️ 4 Production Upgrades in Step 5

  • 1. Multi-Turn Session Memory: Passes conversation history across turns so follow-up questions retain context.
  • 2. Deterministic Safety Guardrail: Cross-checks LLM output against the live marine sensor verdict (UNSAFE_CAVES_CLOSED can never be downgraded!).
  • 3. Execution Telemetry: Tracks latencyMs, toolCallCount, and exact tool inputs/outputs.
  • 4. Resilient Tool Loop: Supports both native Ollama tool calls & fallback JSON tool invocation.
step-05-production-agent/index.ts
const res = await generateWithToolsAndHistory({ systemPrompt: CAPITAO_GIL_PRODUCTION_PROMPT, history: input.history, // Multi-turn conversational state userMessage: input.message, tools: [ searchLagosKnowledge, getLagosWeatherAndMarine, findLagosTours, ], }); // Production Telemetry & Updated History return { answer: res.text, updatedHistory: res.updatedHistory, telemetry: { latencyMs, toolCallCount, model: GEMMA_MODEL }, };
🚀 Run Automated 2-Turn Demo or Interactive Concierge Chat:
🎉 Workshop Recap & Cheat Sheet

Muito Obrigado, DevFest Lagos 2026! 🇵🇹⛵

You just built a 100% local, privacy-preserving Agentic RAG tourism concierge powered by Gemma 4 (gemma4:e2b) and Google Genkit.

⌨️ All 5 Workshop Commands

  • npm run step 1 — Static Prompt & Zod Output Baseline
  • npm run step 2 — Local Vector Store & Grounded RAG
  • npm run step 3 — Live Open-Meteo Weather & Tour Tools
  • npm run step 4 — Autonomous Agentic RAG Pipeline
  • npm run step 5 — Production Multi-Turn Agent & CLI

🧪 Genkit Developer UI Playgrounds

  • npm run step 2 ui — Test Vector Retriever & gemma4:e2b directly in UI
  • npm run step 4 ui — Test searchLagosKnowledge tool & Agentic RAG in UI
  • npm run step 5 -- interactive — Live terminal chat with Capitão Gil
  • npm run ui — Open full workshop workspace at http://localhost:4000
🎤 Questions & Live Experiments • Nuno Andrade (DevFest Lagos 2026)