BrainBit Infotech Teaching · Elective

Build real AI features in Django

One SDK call hands you an AI brain. Django is everything around it — views, templates, routing, and a service layer that keeps your code clean.

🔌 4 modules 🧪 4 simulations 🎓 1 capstone 🆓 Gemini free tier
Press → or Space to begin · F fullscreen · O overview
Why bother with an API

Why AI APIs?

🧠
Rent a ready-made brain: no GPU, no training, no maths — just one SDK call and an intelligent model answers.
🔌
The google-genai SDK wraps all the HTTP for you. Import the client, call generate_content, read response.text.

No boilerplate JSON bodies, no nested candidates digging — the SDK is one import away from clean, readable AI calls.

The road ahead

4 modules + bonus + capstone

🔑

1 · Foundations

SDK setup, service layer, first call

💬

2 · Level 1 Text

text view, template, form

🖼️

3 · Level 2 Vision

Pillow, multimodal, image form

📋

4 · Level 3 JSON

Pydantic, structured output, API

Each module ends in a runnable simulation + a 15-question test (8 coding + 7 concept) on a 15-minute timer. Then: Bonus (memory, provider swap) → Capstone.

Concept · setup

Get a free key & install the SDK

terminal
1 · Go to aistudio.google.com  →  "Get API key"  (free, no card needed)
2 · pip install django python-dotenv google-genai pillow pydantic
3 · Create a .env file in your project root:
      GEMINI_API_KEY=AIza...your-free-key
4 · Add .env to .gitignore — never commit keys
5 · Copy each simulation into ai_test.py and run:  python ai_test.py

Every simulation in this deck is real, runnable code. Because the deck can't call the internet, green boxes show one example reply — your wording will differ (AI is non-deterministic).

🔑
genai.Client() reads GEMINI_API_KEY from the environment automatically — you never pass the key in code.
01
Module 1

Foundations & Service Layer

Set up the project, load your key safely, and build the GeminiService class that the rest of the app will use.

Concept · service layer

Architecture — thin views, one service

project layout
core/          ← Django project (settings, urls)
gemini_app/    ← your app
  services.py  ← ALL AI logic lives here  (GeminiService class)
  views.py     ← thin: call service, render template — no AI code
  urls.py      ← routes
  templates/
🏗️
Why a service layer? Views stay simple and testable. Swap the model or provider by editing only services.py — views never change.

This is the same "keep AI code out of views" pattern used by every production Django+AI project.

Concept · venv + install

Environment setup

terminal
python -m venv venv
venv\Scripts\activate          # Windows  (source venv/bin/activate on Mac/Linux)
pip install django python-dotenv google-genai pillow pydantic

One virtual environment per project — keeps your global Python tidy and lets you lock versions in requirements.txt.

📦
google-genai is the official Google SDK. pillow handles images for multimodal calls. pydantic enforces the JSON schema.
Concept · startproject + startapp

Initialise the Django project

terminal + core/settings.py
django-admin startproject core .
python manage.py startapp gemini_app

# core/settings.py
INSTALLED_APPS = [
    # ... default apps
    'gemini_app',
]

The dot in startproject core . puts manage.py in the current directory instead of a subfolder — the standard layout for this course.

Concept · keep secrets secret

Load your key safely

.env · core/settings.py
# .env  (NEVER committed — add to .gitignore)
GEMINI_API_KEY=AIza...your-free-key

# core/settings.py  (top of file)
import os
from pathlib import Path
from dotenv import load_dotenv

BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env')
# ...
INSTALLED_APPS = [
    # ... default apps
    'gemini_app',
]
🔐
A key pushed to GitHub is found by bots in minutes. load_dotenv(BASE_DIR / '.env') reads it from disk at startup — never hardcoded.

genai.Client() then picks up GEMINI_API_KEY automatically from the environment — you never pass it explicitly.

Concept · services.py

The GeminiService class

gemini_app/services.py
import os
from google import genai
from google.genai import types
from PIL import Image
from pydantic import BaseModel

class GeminiService:
    def __init__(self):
        # Automatically extracts GEMINI_API_KEY from the environment
        self.client = genai.Client()
        self.default_model = "gemini-2.0-flash"

genai.Client() reads GEMINI_API_KEY automatically — no explicit key passing needed. default_model is one place to change the model for all methods.

🏗️
All three levels (text, vision, JSON) will live as methods on this one class — views just call the method they need.
Concept · generate_text

First text call — generate_text

gemini_app/services.py (continued)
    def generate_text(self, prompt: str) -> str:
        """Beginner: Simple text-to-text generation."""
        try:
            response = self.client.models.generate_content(
                model=self.default_model,
                contents=prompt,
            )
            return response.text
        except Exception as e:
            return f"Error generating text: {str(e)}"
▸ example reply
response.text → "Hello! It's great to connect with you today."

response.text is a clean string — no nested dicts. The try/except returns a friendly message instead of crashing the page.

🔌 Simulation · run it in your own terminal

Your first SDK call

ai_test.py
import os
from dotenv import load_dotenv
from google import genai

load_dotenv()   # reads GEMINI_API_KEY from .env

client = genai.Client()           # key read automatically

response = client.models.generate_content(
    model="gemini-2.0-flash",
    contents="Say hello in one short sentence.",
)

print(response.text)
▸ example reply
Hello! It's great to connect with you today.

If you see a sentence, your key works and you've called an AI with the SDK. 🎉

02
Module 2

Level 1 — Text Generator

Wire GeminiService.generate_text into a Django view and a simple HTML form.

Concept · views.py

text_generator_view

gemini_app/views.py
from django.shortcuts import render
from .services import GeminiService

gemini_service = GeminiService()   # instantiate once at module level

def text_generator_view(request):
    context = {}
    if request.method == "POST":
        prompt = request.POST.get("prompt")
        if prompt:
            ai_response = gemini_service.generate_text(prompt)
            context = {"prompt": prompt, "response": ai_response}
    return render(request, "gemini_app/text_gen.html", context)

Instantiate GeminiService once at module level — not inside the view function — so the client is not recreated on every request.

💡
The view is thin: get the prompt, call the service, pass the result to the template. All AI logic stays in services.py.
Concept · template

text_gen.html

templates/gemini_app/text_gen.html
<form method="POST">
  {% csrf_token %}
  <textarea name="prompt" placeholder="Ask anything..."></textarea>
  <button type="submit">Generate</button>
</form>

{% if response %}
  <div class="response-box">
    <h3>AI Response:</h3>
    <p>{{ response }}</p>
  </div>
{% endif %}
🛡️
{% csrf_token %} is required for every Django POST form — it prevents cross-site request forgery attacks.

{{ response }} renders the string returned by generate_text. Django auto-escapes it so user input can't inject HTML.

🔌 Simulation · run it in your own terminal

Text generator — standalone test

ai_test.py
from dotenv import load_dotenv
from google import genai

load_dotenv()

class GeminiService:
    def __init__(self):
        self.client = genai.Client()
        self.default_model = "gemini-2.0-flash"

    def generate_text(self, prompt: str) -> str:
        try:
            response = self.client.models.generate_content(
                model=self.default_model,
                contents=prompt,
            )
            return response.text
        except Exception as e:
            return f"Error generating text: {str(e)}"

svc = GeminiService()
print(svc.generate_text("Explain what a Django view does in 2 sentences."))
▸ example reply
A Django view is a Python function that receives a web request and returns a response. It contains the logic for what data to retrieve and which template to render.
03
Module 3

Level 2 — Vision (Multimodal)

Use Pillow to open an image and pass it directly to the SDK alongside a text prompt.

Concept · multimodal

analyze_image — Pillow + SDK

gemini_app/services.py (continued)
    def analyze_image(self, image_file, prompt: str) -> str:
        """Intermediate: Multimodal analysis using Pillow."""
        try:
            img = Image.open(image_file)
            response = self.client.models.generate_content(
                model=self.default_model,
                contents=[img, prompt],
            )
            return response.text
        except Exception as e:
            return f"Error analyzing image: {str(e)}"

contents=[img, prompt] — passing a list mixes modalities. The SDK handles encoding the Pillow image; you never convert to base64 manually.

🖼️
Gemini 2.0 Flash is multimodal out of the box — the same model, the same generate_content call, just a richer contents list.
Concept · handling uploaded files

image_analyzer_view

gemini_app/views.py (continued)
def image_analyzer_view(request):
    context = {}
    if request.method == "POST" and request.FILES.get("image"):
        image_file = request.FILES["image"]
        prompt = request.POST.get("prompt", "Describe what you see in this image.")
        ai_response = gemini_service.analyze_image(image_file, prompt)
        context = {"response": ai_response, "prompt": prompt}
    return render(request, "gemini_app/image_analyzer.html", context)

request.FILES["image"] is a Django InMemoryUploadedFile — Pillow can open it directly with Image.open(), no temp file needed.

📁
The default prompt fallback means the form works even if the user doesn't type a custom question.
Concept · multipart form

image_analyzer.html — ⚠️ enctype

templates/gemini_app/image_analyzer.html
<form method="POST" enctype="multipart/form-data">
  {% csrf_token %}
  <input type="file" name="image" accept="image/*">
  <input type="text" name="prompt" placeholder="What do you want to know?">
  <button type="submit">Analyze Image</button>
</form>

{% if response %}
  <div class="response-box">
    <p>{{ response }}</p>
  </div>
{% endif %}
⚠️
enctype="multipart/form-data" is mandatory for file uploads — without it, request.FILES will be empty and the view silently skips the upload.
Concept · cost & performance

Image tokens & resizing

services.py — optional resize before send
def analyze_image(self, image_file, prompt: str) -> str:
    img = Image.open(image_file)
    img.thumbnail((1024, 1024))   # shrinks in-place, keeps aspect ratio
    response = self.client.models.generate_content(
        model=self.default_model,
        contents=[img, prompt],
    )
    return response.text

Large images cost more tokens (and time). Calling img.thumbnail((1024, 1024)) before the SDK call reduces cost without losing enough detail for most analysis tasks.

💸
On the free tier, keeping images small means you can run more demos. Rule of thumb: ≤ 1024 px on the longest side.
🔌 Simulation · run it in your own terminal

Image analyzer — standalone test

ai_test.py
from dotenv import load_dotenv
from google import genai
from PIL import Image

load_dotenv()

client = genai.Client()
img = Image.open("test_image.jpg")   # any image file you have

response = client.models.generate_content(
    model="gemini-2.0-flash",
    contents=[img, "Describe what you see in this image."],
)

print(response.text)
▸ example reply
The image shows a sunny park with green trees, a winding path, and a few people walking in the distance.

Drop any .jpg or .png into the same folder, rename it test_image.jpg, and run. The model describes whatever is in the photo.

04
Module 4

Level 3 — Structured JSON

Use Pydantic to define a schema and guarantee the model returns JSON that exactly matches your data contract.

Concept · data contracts

Why Pydantic — not regex or string-parse

the problem with prompting for JSON
# Without a schema the model might return:
#   '{"recipeName": ...}'     ← wrong key name
#   '```json\n{...}\n```'    ← markdown fence you must strip
#   '{ingredients: [...] }'  ← invalid JSON (unquoted keys)
#
# With response_schema=RecipeSchema the SDK guarantees:
#   - valid JSON
#   - exact field names you declared
#   - correct types (str, list[str], int)
📋
Pydantic turns a Python class into a JSON schema. Pass that schema to GenerateContentConfig and the model is forced to conform — no parsing tricks needed.

This is the difference between "ask nicely and hope" and a real data contract.

Concept · Pydantic schema

RecipeSchema

gemini_app/views.py
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from pydantic import BaseModel, Field

class RecipeSchema(BaseModel):
    recipe_name: str = Field(description="The name of the dish")
    ingredients: list[str] = Field(description="List of ingredients with measurements")
    prep_time_minutes: int = Field(description="Total prep and cooking time in minutes")

Three typed fields: a string, a list of strings, and an integer. The model must return all three with these exact names and types.

🔒
Field(description=...) gives the model a hint about what each field should contain — better descriptions lead to better AI output.
Concept · GenerateContentConfig

generate_structured_json

gemini_app/services.py (continued)
    def generate_structured_json(self, prompt: str, schema: type[BaseModel]) -> str:
        """Advanced: Guarantees JSON conforming to a Pydantic model."""
        try:
            response = self.client.models.generate_content(
                model=self.default_model,
                contents=prompt,
                config=types.GenerateContentConfig(
                    response_mime_type="application/json",
                    response_schema=schema,
                ),
            )
            return response.text
        except Exception as e:
            return f"Error generating structured JSON: {str(e)}"

response_mime_type="application/json" + response_schema=schema in GenerateContentConfig — these two settings lock the output format.

Concept · JSON API view

api_generate_recipe

gemini_app/views.py (continued)
@csrf_exempt  # demo only; secure via Token/Session auth in production
def api_generate_recipe(request):
    if request.method != "POST":
        return JsonResponse({"error": "Only POST requests are allowed"}, status=405)
    try:
        data = json.loads(request.body)
        user_ingredient = data.get("ingredient", "eggs")
    except json.JSONDecodeError:
        return JsonResponse({"error": "Invalid JSON payload"}, status=400)

    prompt = f"Give me a quick, creative recipe centered around: {user_ingredient}."
    raw_json_string = gemini_service.generate_structured_json(prompt, RecipeSchema)
    try:
        clean_json_data = json.loads(raw_json_string)
        return JsonResponse(clean_json_data, status=200)
    except json.JSONDecodeError:
        return JsonResponse({"error": "AI returned malformed JSON structure"}, status=500)

Parse JSON body → call the service → parse the string back to a dict → return as JsonResponse. Callers get clean, typed JSON.

🔌 Simulation · run it in your own terminal

Structured JSON recipe — standalone test

ai_test.py
import json
from dotenv import load_dotenv
from google import genai
from google.genai import types
from pydantic import BaseModel, Field

load_dotenv()
client = genai.Client()

class RecipeSchema(BaseModel):
    recipe_name: str = Field(description="The name of the dish")
    ingredients: list[str] = Field(description="List of ingredients with measurements")
    prep_time_minutes: int = Field(description="Total prep and cooking time in minutes")

response = client.models.generate_content(
    model="gemini-2.0-flash",
    contents="Give me a quick recipe centered around: eggs.",
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        response_schema=RecipeSchema,
    ),
)

recipe = json.loads(response.text)
print(recipe["recipe_name"])
print(recipe["ingredients"])
print(recipe["prep_time_minutes"], "minutes")
▸ example reply
Scrambled Eggs Delight
['2 eggs', '1 tbsp butter', 'salt', 'pepper', '2 tbsp milk']
10 minutes
Bonus

Beyond the Basics

Conversation memory, provider swapping, and production best practices — the extras that take your app from demo to deployable.

Concept · chat memory

Conversation memory — SDK chats

SDK chat session
# Conversation memory: the SDK keeps history for you
chat = client.chats.create(model="gemini-2.0-flash")
print(chat.send_message("What is a variable?").text)
print(chat.send_message("Give an example.").text)   # remembers the first turn
▸ example
Turn 1: A variable is a named container that holds a value in memory.
Turn 2: Sure! x = 5 stores the integer 5 — you can use x anywhere in your code.

client.chats.create() returns a stateful session. The SDK automatically resends the conversation history on every send_message call — you don't manage the history list yourself.

Concept · provider-agnostic

Swap the provider

same service layer, different SDK
# Swap provider — OpenAI
from openai import OpenAI
oai = OpenAI()
r = oai.chat.completions.create(model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}])
print(r.choices[0].message.content)

# Swap provider — Anthropic
from anthropic import Anthropic
ac = Anthropic()
m = ac.messages.create(model="claude-haiku-4-5", max_tokens=1024,
        messages=[{"role": "user", "content": prompt}])
print(m.content[0].text)

Because all AI logic lives in services.py, changing providers means editing one file. Views and templates are completely untouched.

🔁
Build once with Gemini (free tier); swap to GPT-4o or Claude in minutes when price or quality demands it.
Concept · production

Production best practices

Async / Celery
Django's WSGI is synchronous — long AI calls block a worker thread. Offload to Celery or use async def views for production traffic.
Token optimisation
Trim inputs before sending. Cache frequent responses. Use gemini-2.0-flash for simple tasks — heavier models only when quality demands it.
Error boundaries + 429
Wrap every SDK call in try/except. On 429 (rate-limit), back off and retry with exponential delay. Always return a friendly fallback string — never let an AI error reach the browser as a 500.
Concept · URLs

Capstone step 1 — routing

gemini_app/urls.py · core/urls.py
# gemini_app/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('text-gen/', views.text_generator_view, name='text_gen'),
    path('image-analyze/', views.image_analyzer_view, name='image_analyze'),
    path('api/recipe/', views.api_generate_recipe, name='api_recipe'),
]

# core/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('ai/', include('gemini_app.urls')),
]

Three endpoints under /ai/. The project urls include the app urls — the standard Django pattern for reusable apps.

Concept · run & verify

Capstone step 2 — run & test endpoints

terminal
python manage.py migrate
python manage.py runserver

# Endpoints now live at:
#   http://127.0.0.1:8000/ai/text-gen/       ← text form
#   http://127.0.0.1:8000/ai/image-analyze/  ← image upload form
#   http://127.0.0.1:8000/ai/api/recipe/     ← JSON API (POST)

# Quick API test:
curl -X POST http://127.0.0.1:8000/ai/api/recipe/ \
     -H "Content-Type: application/json" \
     -d "{\"ingredient\": \"avocado\"}"
▸ example JSON reply
{"recipe_name": "Avocado Toast Delight",
 "ingredients": ["1 avocado", "2 slices bread", "salt", "lemon"],
 "prep_time_minutes": 5}
🎓 You built it

The end-to-end flow

▸ your app does this
form → view → GeminiService → SDK → response.text → template / JsonResponse

Three levels of intelligence in one Django app — text generation, multimodal vision, and structured JSON — all through the same GeminiService. Change the prompt or schema and you have a new product.

Recap

Concept → Project

SDK + GeminiService — one client, three levels, swappable provider
Level 1 Text — generate_text → view → template
Level 2 Vision — Pillow + contents=[img, prompt]
Level 3 JSON — Pydantic schema → GenerateContentConfig
Your turn

Practice Challenges

Course complete

You're Ready

You can set up the google-genai SDK, build a service layer, generate text, analyse images with Pillow, and return Pydantic-validated JSON — all wired into a real Django app. That's the whole stack.

Take the final test → ask your instructor for the link.

BrainBit Infotech Teaching, a unit of BrainBit Infotech Private Limited · Ranchi

All slides — click to jump (press O or Esc to close)

BrainBit Infotech Teaching· AI APIs in Django
→ / Space · next
←  ·  back  ·  F  ·  fullscreen