# Atelier : construire une app d'IA avec Gemma 4, pas à pas

En 45 minutes, on construit une petite API qui envoie la photo d'une feuille de maïs à Gemma 4 et renvoie un diagnostic en JSON. On applique au passage le guide « Coder avec l'IA : le guide complet, du premier prompt à la production » : fichiers de contexte, prompts rangés, sortie structurée, tests.

## Ce qu'on construit

**AgriScan** : une API qui reçoit la photo d'une feuille de maïs et répond avec un diagnostic simple, en français, au format JSON.

```json
{"maladie": "rouille commune", "confiance": "moyenne", "conseil": "Retire les feuilles touchées et surveille les plants voisins."}
```

Le trajet d'une requête : l'utilisateur envoie une photo → FastAPI la vérifie → `gemma.py` l'envoie à Gemma 4 avec le prompt de `prompts.py` → on lit le JSON → on le renvoie.

Gemma 4 est un modèle *open-weight* de Google qui comprend le texte et les images. On l'appelle ici via l'API Gemini, ce qui évite de le faire tourner sur ton ordinateur.

> Ce projet sert d'exemple. Pendant le Hack Day, remplace le maïs par ton propre problème : la structure reste la même.

## Prérequis

- Python 3.11 ou plus récent (`python --version`)
- Git et un compte GitHub
- Un éditeur, idéalement VS Code avec GitHub Copilot
- Une clé d'API Gemini, gratuite, créée sur [Google AI Studio](https://aistudio.google.com/) (bouton *Get API key*)
- Une ou deux photos de feuilles pour tester (n'importe quelle plante convient)

## Étape 1 : créer le dépôt et ses fichiers de contexte

Sur GitHub, crée un dépôt **public** `agriscan` avec un README, une licence **MIT** et le `.gitignore` *Python*. Puis :

```bash
git clone https://github.com/TON-PSEUDO/agriscan.git
cd agriscan
mkdir app tests
touch app/__init__.py app/main.py app/gemma.py app/prompts.py tests/test_main.py
```

Ajoute `AGENTS.md` à la racine, avant d'écrire le moindre code. C'est lui qui guidera Copilot pendant tout l'atelier (modèle complet dans le guide) :

```markdown
# Instructions pour l'assistant IA

## Le projet
AgriScan : une API qui diagnostique une maladie du maïs à partir d'une photo.

## Stack
- Python 3.11, FastAPI
- Gemma 4 via l'API Gemini, SDK google-genai, modèle gemma-4-26b-a4b-it

## Commandes
- Lancer : fastapi dev app/main.py
- Tester : pytest

## Règles
- La clé d'API vient de GEMINI_API_KEY, jamais du code.
- Tous les appels au modèle passent par app/gemma.py.
- Tous les prompts sont dans app/prompts.py.
- Chaque route a un test pytest qui n'appelle pas le vrai modèle.
```

Crée aussi `requirements.txt` :

```text
fastapi[standard]
google-genai
pytest
```

## Étape 2 : installer et configurer la clé d'API

```bash
python -m venv .venv
source .venv/bin/activate        # Windows : .venv\Scripts\activate
pip install -r requirements.txt
```

Le SDK lit la clé dans la variable d'environnement `GEMINI_API_KEY` :

```bash
export GEMINI_API_KEY="ta-cle"          # macOS / Linux
$env:GEMINI_API_KEY="ta-cle"            # Windows PowerShell
```

Ne l'écris jamais dans un fichier du dépôt. Si tu la mets dans un fichier `.env`, vérifie que `.env` figure bien dans `.gitignore`.

## Étape 3 : premier appel à Gemma 4

Crée un fichier `essai.py` à la racine :

```python
from google import genai

client = genai.Client()  # lit GEMINI_API_KEY

response = client.models.generate_content(
    model="gemma-4-26b-a4b-it",
    contents="Explique en deux phrases ce qu'est un modèle open-weight.",
)
print(response.text)
```

```bash
python essai.py
```

Si une réponse s'affiche, tout est en place. Deux modèles Gemma 4 sont disponibles via l'API Gemini : `gemma-4-26b-a4b-it` (celui qu'on utilise) et `gemma-4-31b-it`. Commit maintenant :

```bash
git add . && git commit -m "feat: premier appel a Gemma 4"
```

## Étape 4 : ranger le prompt dans prompts.py

Le prompt est du code : on le met à un seul endroit, on le versionne, on le teste. Dans `app/prompts.py` :

```python
DIAGNOSTIC_PROMPT = """Tu es un agronome qui aide des petits producteurs au Togo.
On te montre la photo d'une feuille de maïs.
Réponds uniquement avec un objet JSON, sans texte autour, avec les clés :
- "maladie" : le nom probable de la maladie, ou "inconnue"
- "confiance" : "faible", "moyenne" ou "forte"
- "conseil" : une action simple, en une phrase, en français
Si la photo ne montre pas une feuille de maïs, réponds
{"maladie": "photo non valide", "confiance": "forte", "conseil": "Envoie une photo nette d'une feuille de maïs."}"""
```

On applique ici trois techniques du guide : un **rôle** et un public, un **format de sortie** imposé, et un **cas limite** prévu à l'avance.

## Étape 5 : envoyer une image à Gemma 4

Dans `app/gemma.py`, un seul endroit appelle le modèle. Le prompt passe en *system instruction* ; l'image est envoyée comme une partie du contenu.

```python
import json

from google import genai
from google.genai import types

from app.prompts import DIAGNOSTIC_PROMPT

MODEL = "gemma-4-26b-a4b-it"


def analyser_image(image_bytes: bytes, mime_type: str) -> dict:
    """Envoie la photo à Gemma 4 et renvoie le diagnostic sous forme de dict."""
    client = genai.Client()  # créé ici pour que les tests n'aient pas besoin de clé
    response = client.models.generate_content(
        model=MODEL,
        config=types.GenerateContentConfig(system_instruction=DIAGNOSTIC_PROMPT),
        contents=[
            types.Part.from_bytes(data=image_bytes, mime_type=mime_type),
            "Analyse cette photo.",
        ],
    )
    return extraire_json(response.text)
```

## Étape 6 : lire une sortie JSON fiable

Même quand on demande « uniquement du JSON », un modèle ajoute parfois des balises `` ```json `` ou une phrase autour. On extrait donc l'objet entre la première accolade et la dernière, et on refuse proprement le reste. Ajoute dans `app/gemma.py` :

```python
def extraire_json(texte: str) -> dict:
    """Extrait le premier objet JSON d'une réponse du modèle."""
    debut, fin = texte.find("{"), texte.rfind("}")
    if debut == -1 or fin == -1:
        raise ValueError(f"Pas de JSON dans la réponse : {texte[:200]}")
    return json.loads(texte[debut : fin + 1])
```

Teste-le vite dans un terminal Python avec une de tes photos :

```python
from app.gemma import analyser_image
print(analyser_image(open("feuille.jpg", "rb").read(), "image/jpeg"))
```

Essaie aussi une photo qui n'a rien à voir (ton chat, un ordinateur) pour vérifier que le cas « photo non valide » fonctionne. Si la réponse ne te plaît pas, c'est le prompt qu'on corrige, pas le code.

## Étape 7 : exposer une API avec FastAPI

On transforme la fonction en route web. `app/main.py` vérifie l'entrée (type et taille de la photo), appelle Gemma et renvoie une erreur claire si quelque chose casse :

```python
from fastapi import FastAPI, File, HTTPException, UploadFile

from app import gemma

app = FastAPI(title="AgriScan")
TAILLE_MAX = 5 * 1024 * 1024  # 5 Mo


@app.post("/diagnostic")
async def diagnostic(photo: UploadFile = File(...)):
    if photo.content_type not in {"image/jpeg", "image/png"}:
        raise HTTPException(415, "Envoie une photo JPEG ou PNG.")
    contenu = await photo.read()
    if len(contenu) > TAILLE_MAX:
        raise HTTPException(413, "Photo trop lourde : 5 Mo maximum.")
    try:
        return gemma.analyser_image(contenu, photo.content_type)
    except ValueError:
        raise HTTPException(502, "Le modèle n'a pas renvoyé un résultat lisible. Réessaie.")
```

Note : on écrit `gemma.analyser_image(...)` et pas `from app.gemma import analyser_image`. Ce détail permet au test de l'étape 8 de remplacer la fonction.

Lance le serveur :

```bash
fastapi dev app/main.py
```

Ouvre http://127.0.0.1:8000/docs, clique sur **POST /diagnostic**, puis **Try it out**, choisis une photo et clique **Execute**. Tu dois voir le JSON du diagnostic. FastAPI génère cette page tout seul : c'est aussi ta démo pour la vidéo.

## Étape 8 : écrire un test qui ne dépend pas du modèle

Un test qui appelle vraiment Gemma est lent, coûte du quota et échoue sans réseau. On remplace donc l'appel au modèle par une fausse réponse avec `monkeypatch`, et on teste notre code à nous. `tests/test_main.py` :

````python
from fastapi.testclient import TestClient

from app import gemma
from app.main import app

client = TestClient(app)


def test_diagnostic(monkeypatch):
    faux = {"maladie": "rouille", "confiance": "moyenne", "conseil": "..."}
    monkeypatch.setattr(gemma, "analyser_image", lambda data, mime: faux)
    r = client.post("/diagnostic", files={"photo": ("feuille.jpg", b"fake", "image/jpeg")})
    assert r.status_code == 200
    assert r.json()["maladie"] == "rouille"


def test_refuse_un_pdf():
    r = client.post("/diagnostic", files={"photo": ("doc.pdf", b"%PDF", "application/pdf")})
    assert r.status_code == 415


def test_extraire_json_avec_texte_autour():
    texte = 'Voici le résultat :\n```json\n{"maladie": "inconnue"}\n```'
    assert gemma.extraire_json(texte) == {"maladie": "inconnue"}
````

Lance les tests, puis commite :

```bash
pytest
git add .
git commit -m "Ajoute la route /diagnostic et ses tests"
git push
```

Trois tests verts, aucune clé d'API nécessaire. C'est exactement ce que le guide appelait « vérifier avant de faire confiance ».

## Étape 9 : avancer plus vite avec Copilot (et ton AGENTS.md)

Ton `AGENTS.md` de l'étape 2 travaille maintenant pour toi : Copilot le lit et connaît déjà le modèle, la structure et les commandes. Tes prompts peuvent donc être courts et précis. Quelques exemples à coller dans Copilot Chat (mode agent) :

```text
Ajoute une page HTML simple dans app/static/index.html avec un champ pour choisir
une photo et un bouton « Analyser ». Elle appelle POST /diagnostic et affiche
maladie, confiance et conseil. Pas de framework JavaScript. Sers-la avec FastAPI.
```

```text
Dans app/main.py, si le modèle renvoie "photo non valide", réponds avec un code 422
et le conseil du modèle. Ajoute le test correspondant dans tests/test_main.py.
```

```text
Relis app/gemma.py et liste les cas d'erreur non gérés. Ne modifie rien,
explique seulement.
```

Après chaque modification : relis le diff, lance `pytest`, essaie dans `/docs`, puis commite. Si Copilot part dans une mauvaise direction, annule (`git restore .`) et reformule plutôt que de corriger à la main un code que tu ne comprends pas.

## Étape 10 : README, vidéo et soumission

Le jury regarde ton repo avant ton pitch. Ton `README.md` doit répondre à ces questions :

- **Le problème** : qui en souffre, en une ou deux phrases.
- **La solution** : ce que fait l'app et le rôle exact de Gemma 4.
- **Lancer le projet** : les commandes de l'étape 3 et de l'étape 7, copiables.
- **Le modèle** : `gemma-4-26b-a4b-it` via l'API Gemini, et sa licence.
- **Outils IA utilisés** : par exemple « GitHub Copilot pour la page HTML et les tests ; Gemma 4 pour le diagnostic ».
- **L'équipe** : noms et profils GitHub.
- **La vidéo démo** : le lien.

Pour la vidéo (2 minutes maximum) : enregistre ton écran pendant que tu envoies une vraie photo et que le diagnostic s'affiche. Dépose-la sur Google Drive, règle le partage sur **« Tous les utilisateurs disposant du lien »** et colle le lien dans le README. Ouvre ce lien dans une fenêtre privée pour vérifier qu'il marche.

Enfin, sur OrganizerHQ : **Challenges**, puis **Add Submission**, coche les défis visés (Best Open-Source AI Project, Best Use of Gemma, Best Use of GitHub Copilot), colle l'URL du repo, puis **Submit Project**. Le Hack Day ferme les soumissions à 12:35 : vise 12:20 pour garder une marge.

Dernière vérification avant de soumettre :

```bash
git status   # rien à commiter
git log --oneline | head   # tes commits sont là
```

Puis ouvre ton repo sur GitHub depuis un autre navigateur : il doit être public, avec un fichier LICENSE.

## Dépannage

| Symptôme | Cause probable | Que faire |
| --- | --- | --- |
| `401`, `API key not valid` | Clé absente ou mal copiée | Relance `export GEMINI_API_KEY=...` dans le terminal qui lance le serveur, sans guillemets en trop. |
| `429`, `RESOURCE_EXHAUSTED` | Quota gratuit dépassé | Attends une minute, évite les appels en boucle, et utilise les tests avec `monkeypatch` pendant le développement. |
| `404`, modèle introuvable | Mauvais identifiant | Vérifie le nom exact du modèle dans Google AI Studio et dans `MODEL`. |
| `ValueError: Pas de JSON` | Le modèle a répondu en texte libre | Renforce le prompt (« uniquement un objet JSON »), affiche `response.text` pour voir ce qu'il a dit. |
| `ModuleNotFoundError: app` | Mauvais dossier | Lance les commandes depuis la racine du repo, là où se trouve `app/`. |
| `415` sur une vraie photo | Format HEIC (iPhone) ou WebP | Convertis en JPEG, ou ajoute le type dans la liste autorisée. |

## Ressources

- Gemma sur l'API Gemini, documentation officielle : https://ai.google.dev/gemma/docs/core/gemma_on_gemini_api
- Google AI Studio, pour créer ta clé et tester tes prompts : https://aistudio.google.com
- Démarrer avec Gemma (MLH) : https://mlh.link/gemma-quickstart
- Guide débutant Gemma (MLH) : https://mlh.link/gemma-beginnerguide
- Documentation Gemma (MLH) : https://mlh.link/gemma-docs
- Gemma 4 avec l'API Gemini, tutoriel de Philipp Schmid : https://www.philschmid.de/gemma-4-gemini-api
- GitHub Copilot gratuit pour les étudiants : https://mlh.link/GitHub
- Documentation FastAPI : https://fastapi.tiangolo.com
- Le guide compagnon de cet atelier : *Coder avec l'IA : le guide complet, du premier prompt à la production*

Une question pendant le hack ? Lève la main, un mentor passe. Après le Hack Day, on continue sur le Discord de Python Togo : https://pytogo.org/discord

