Agentic farm advisory assistant built with Gemma 4 + Google AI Studio A developer built an agentic farm advisory assistant using Gemma 4 and Google AI Studio's Gemini API. The assistant diagnoses crop issues from photos, checks weather-based planting windows, and logs farm activity through real function-calling, prototyped in the browser and shipped as an Express backend with a chat UI. We will walk through a complete, working project: an agentic farm advisory assistant built with Gemma 4 through Google AI Studio's Gemini API. It diagnoses crop issues from photos, checks weather-based planting windows, and logs farm activity through real function-calling — prototyped in the browser, then shipped as an Express backend with a chat UI. An advisory chatbot for smallholder farmers and agro-logistics platforms that can: The model never guesses market prices or weather data — every factual answer comes from an actual function call against a backend, not the model's own assumptions. Before writing any code, the entire agent was designed inside aistudio.google.com: gemma-4-31b-it for its multimodal image support You are a friendly, practical farm advisory assistant for smallholder farmers in Nigeria. Always use the provided tools for weather checks, market prices, and activity logging — never guess prices or weather data. When a farmer uploads a crop photo, examine it carefully before giving diagnosis and next steps. Keep responses short, practical, and in plain language. Reply in the same language or Pidgin the farmer writes in. check weather window , get market price , log farm activity , and diagnose crop image — each with a JSON schema and a scoped description check weather window instead of answering from general knowledge @google/genai Testing the image diagnosis directly in the browser first was the most valuable step — it's far easier to spot a vague description problem "model just said 'looks unhealthy'" in a live chat than after it's buried in server logs. js // tools.js export const tools = { functionDeclarations: { name: "check weather window", description: "Use when the farmer asks if it's safe or a good time to plant, spray, or harvest. Never guess weather.", parameters: { type: "OBJECT", properties: { location: { type: "STRING", description: "e.g. Port Harcourt, Owerri" }, activity: { type: "STRING", description: "planting, spraying, or harvesting" } }, required: "location", "activity" } }, { name: "get market price", description: "Use ONLY when the farmer asks for the current price of a crop. Never guess a price.", parameters: { type: "OBJECT", properties: { crop: { type: "STRING", description: "e.g. cassava, maize, tomato" }, market: { type: "STRING", description: "e.g. Mile 1 Market, Port Harcourt" } }, required: "crop" } }, { name: "log farm activity", description: "Use when the farmer reports completing an activity like planting, spraying, or harvesting.", parameters: { type: "OBJECT", properties: { farmerId: { type: "STRING" }, activity: { type: "STRING", description: "planting, spraying, harvesting" }, crop: { type: "STRING" }, notes: { type: "STRING" } }, required: "farmerId", "activity", "crop" } }, { name: "diagnose crop image", description: "Use when the farmer uploads a photo of a crop showing signs of disease, pest damage, or poor health.", parameters: { type: "OBJECT", properties: { crop: { type: "STRING", description: "e.g. maize, tomato, cassava" }, symptomDescription: { type: "STRING", description: "Visible symptoms described from the image" } }, required: "crop", "symptomDescription" } } } ; const weatherData = { "port harcourt": { rainChance: 20, condition: "clear, light wind" }, "owerri": { rainChance: 70, condition: "heavy rain expected" } }; const marketPrices = { cassava: { pricePerBag: 18500, currency: "NGN", market: "Mile 1 Market" }, maize: { pricePerBag: 22000, currency: "NGN", market: "Mile 1 Market" }, tomato: { pricePerBasket: 15000, currency: "NGN", market: "Mile 1 Market" } }; const activityLog = ; export async function check weather window { location, activity } { const key = location.toLowerCase ; const weather = weatherData key ; if weather return { error: "Location not found in weather data" }; const safe = activity === "spraying" ? weather.rainChance < 40 : true; return { location, activity, ...weather, recommendation: safe ? "Safe to proceed" : "Wait — rain expected, risk of runoff" }; } export async function get market price { crop, market } { const price = marketPrices crop.toLowerCase ; return price ? { crop, ...price } : { error: No price data for ${crop} }; } export async function log farm activity { farmerId, activity, crop, notes } { const entry = { farmerId, activity, crop, notes: notes || "", date: "2026-07-07" }; activityLog.push entry ; return { logged: true, ...entry }; } export async function diagnose crop image { crop, symptomDescription } { // In production, this would call a vision model or trained classifier. // Here Gemma 4's own multimodal reasoning already produced symptomDescription // from the uploaded image before calling this tool. const knownIssues = { "brown spots on leaves": "Likely leaf blight — recommend copper-based fungicide, improve drainage", "wilting despite watering": "Possible bacterial wilt or root rot — check soil drainage and remove affected plants" }; const match = Object.keys knownIssues .find k = symptomDescription.toLowerCase .includes k.split " " 0 ; return { crop, diagnosis: match ? knownIssues match : "Symptoms noted but inconclusive — recommend local extension officer visit", }; } export const toolFunctions = { check weather window, get market price, log farm activity, diagnose crop image }; The core of the backend is a loop that keeps resolving tool calls — including image-based diagnosis — until Gemma 4 returns a final plain-text answer: python // server.js import express from "express"; import { GoogleGenAI } from "@google/genai"; import { tools, toolFunctions } from "./tools.js"; const app = express ; app.use express.json { limit: "10mb" } ; // allow base64 image payloads const client = new GoogleGenAI { apiKey: process.env.GEMINI API KEY } ; const MODEL = "gemma-4-31b-it"; const MAX STEPS = 4; const SYSTEM INSTRUCTION = You are a friendly, practical farm advisory assistant... ; const sessions = new Map ; app.post "/api/chat", async req, res = { const { sessionId = "default", message, imageBase64 } = req.body; if sessions.has sessionId { sessions.set sessionId, client.chats.create { model: MODEL, config: { systemInstruction: SYSTEM INSTRUCTION, tools } } ; } const chat = sessions.get sessionId ; const parts = imageBase64 ? { inlineData: { mimeType: "image/jpeg", data: imageBase64 } }, { text: message } : message; let response = await chat.sendMessage { message: parts } ; let steps = 0; while response.functionCalls?.length && steps < MAX STEPS { const call = response.functionCalls 0 ; const result = await toolFunctions call.name call.args ; response = await chat.sendMessage { message: { functionResponse: { name: call.name, response: result } } } ; steps += 1; } res.json { reply: response.text, toolStepsUsed: steps } ; } ; app.listen 3000, = console.log "Agro advisory agent running on port 3000" ; With the server running npm start and a GEMINI API KEY from AI Studio set in .env , here's what real requests return. Request — weather check before spraying: curl -X POST http://localhost:3000/api/chat \ -H "Content-Type: application/json" \ -d '{"sessionId":"farmer1","message":"Is it safe to spray my maize in Owerri today?"}' Response: { "reply": "Not today — Owerri has a 70% chance of rain, which could wash off the spray before it works. Wait for a drier day.", "toolStepsUsed": 1 } Request — market price check: curl -X POST http://localhost:3000/api/chat \ -H "Content-Type: application/json" \ -d '{"sessionId":"farmer1","message":"How much is cassava selling for now?"}' Response: { "reply": "Cassava is currently going for NGN 18,500 per bag at Mile 1 Market.", "toolStepsUsed": 1 } Request — crop photo diagnosis image + text : curl -X POST http://localhost:3000/api/chat \ -H "Content-Type: application/json" \ -d '{"sessionId":"farmer1","message":"My tomato plant looks sick, see photo","imageBase64":"