Build an AI Agent with Ollama and Django

Build a local AI agent chatbot with tool calling and real-time streaming.

Introduction

30 Second Summary

When you ask an AI assistant to check the weather or do a calculation, it doesn't just guess the answer. It calls a real function behind the scenes, gets the result, and weaves it into a natural response.

In this project, you will build your own AI agent that runs entirely on your local machine. Your agent will decide when to call Python functions you define, execute them, and stream responses in real-time through a web chat interface.

What You'll Build

A Django web application that hosts a local AI agent powered by Ollama, with custom tool calling and real-time streaming responses.

By the end of this project, you'll have:

  • A Django project with a chat view and a streaming endpoint.
  • An AI agent service that manages conversation history and executes tool calls through an agent loop.
  • Custom tool functions (a calculator and a knowledge base lookup) that the AI invokes autonomously.
  • A real-time chat interface with token-by-token streaming, just like ChatGPT.
  • Secret Mission: Add a task planner tool that breaks complex requests into step-by-step plans stored in a database.

Are there any prerequisites?

You should have basic Python knowledge (variables, functions, loops) and a general understanding of how web applications work. No prior experience with Django or AI tools is required.

Set Up Your Django Project

Your environment is ready with Python, Ollama, and Django all installed. Now it's time to structure your Django project so it's ready to host the AI agent.

Django organizes code into "apps." Each app handles one piece of functionality. You'll create an agent app to contain all the AI agent logic, then wire it into the project.

In this step, get ready to:
  • Create a Django app called agent.
  • Register the app in your project settings.
  • Wire up the URL configuration for the agent app.
Create the agent app

Django apps are self-contained modules that group related functionality together. Your agent app will hold the AI service, tools, views, and templates for the chatbot.

  • In your terminal, make sure you're inside the myproject folder (the one where manage.py lives).
  • Create the agent app by running this command:
python manage.py startapp agent

What does this command do?

The startapp command generates a new folder called agent/ with boilerplate files Django needs to recognize it as an app.

You'll see files like __init__.py, apps.py, models.py, views.py, and a migrations/ folder. You'll build on several of these files throughout the project.

  • Confirm the app was created by running:
ls agent/
  • You should see __init__.py, apps.py, models.py, views.py, and other files listed in the output.

Command not found or ModuleNotFoundError?

Make sure your virtual environment is still activated. You should see the environment name in parentheses at the start of your terminal prompt.

If python doesn't work on Windows, try py manage.py startapp agent instead.

Help me troubleshoot the startapp command failing

Register the app in settings

Creating the app folder isn't enough on its own. Django needs to know about the app through its settings file before it can use it.

  • In your editor, open myproject/settings.py.
  • Find the INSTALLED_APPS list. It looks like this:
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]
  • Add "agent" as the last item in the list, so it looks like this:
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "agent",
]

Why register the app?

Django uses INSTALLED_APPS to know which apps to include when running the server, applying migrations, and loading templates. Without this line, Django would ignore your agent app entirely.

  • Save myproject/settings.py.
Wire up the URL configuration

Django routes incoming web requests through URL patterns. You need to tell the project to forward requests to your agent app's URLs.

  • Open myproject/urls.py in your editor.
  • Replace the entire contents of the file with the following code:
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("agent.urls")),
]

What does this code do?

  • include lets you reference URL patterns defined in another app's urls.py file.
  • path("", include("agent.urls")) sends all requests at the root path to your agent app's URL configuration.
  • You'll create agent/urls.py in a later step. For now, this sets up the wiring so everything connects cleanly.
  • Save myproject/urls.py.

Don't run the server yet

The server will throw an error if you start it now because agent/urls.py doesn't exist yet. You'll create it in a later step.

✔️ Awesome, I've got everything!

Great. Double check you've saved both myproject/settings.py and myproject/urls.py.

ⓧ I'd like to double check the full code

Here is myproject/settings.py in full. Your SECRET_KEY value may be different, and that's fine.

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = "django-insecure-dev-key-change-in-production"

DEBUG = True

ALLOWED_HOSTS = []

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "agent",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "myproject.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "myproject.wsgi.application"

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

AUTH_PASSWORD_VALIDATORS = [
    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True

STATIC_URL = "static/"

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

And here is myproject/urls.py in full:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("agent.urls")),
]

Your Django project now knows about the agent app. Next up, you'll build the AI agent service that powers the chatbot's brain.

Build the AI Agent Service

Your Django project is scaffolded, but right now it's just an empty shell. The real power of an AI agent comes from two things: the tools it can use, and the loop that decides when to use them.

In this step, you'll build the brain of your AI agent. You'll create a service class that manages conversations, calls the Ollama model, detects when the model wants to use a tool, executes that tool, and feeds the result back. You'll also define the tools themselves.

In this step, get ready to:
  • Create an agent service that orchestrates the conversation loop between the user, the LLM, and the tools.
  • Define two tool functions (a calculator and a knowledge base lookup) that give the AI real capabilities.
Create the agent service module

The agent service is the orchestrator that ties everything together. It holds the conversation history, knows which tools are available, and implements the agent loop pattern.

  • Create a new file called agent/services.py in your agent folder and add the following code:
import json
from ollama import chat


SYSTEM_PROMPT = """You are a helpful AI assistant with access to tools. 
When a user asks a question that requires calculation, use the calculate tool.
When a user asks about a topic that might be in the knowledge base, use the lookup_knowledge tool.
Always use tools when they are relevant to the user's question.
Provide clear, concise answers based on the tool results."""


class AgentService:
    def __init__(self, model: str = "qwen3:8b"):
        self.model = model
        self.messages = [{"role": "system", "content": SYSTEM_PROMPT}]
        self.tools = []
        self.available_functions = {}

    def register_tool(self, func):
        """Register a Python function as a tool the agent can call."""
        self.tools.append(func)
        self.available_functions[func.__name__] = func

What does this code do?

This sets up the foundation of your agent service.

  • SYSTEM_PROMPT tells the LLM what it is and when to use its tools. This prompt is sent with every conversation.
  • __init__ creates empty lists for tools and a message history that starts with the system prompt.
  • register_tool lets you pass in any Python function and make it available to the AI. The Ollama SDK automatically generates the tool schema from the function's type hints and docstring.

Now you'll add the core agent loop method. This is the pattern that makes an AI agent different from a simple chatbot.

  • Below the register_tool method, add the chat and clear_history methods:
    def chat(self, user_message: str):
        """Send a message and run the agent loop. Yields response chunks."""
        self.messages.append({"role": "user", "content": user_message})

        while True:
            response = chat(
                model=self.model,
                messages=self.messages,
                tools=self.tools,
                stream=True,
                think=True,
            )

            content = ""
            tool_calls = []

            for chunk in response:
                if chunk.message.content:
                    content += chunk.message.content
                    yield chunk.message.content
                if chunk.message.tool_calls:
                    tool_calls.extend(chunk.message.tool_calls)

            if content or tool_calls:
                assistant_message = {"role": "assistant", "content": content}
                if tool_calls:
                    assistant_message["tool_calls"] = tool_calls
                self.messages.append(assistant_message)

            if not tool_calls:
                break

            for call in tool_calls:
                func_name = call.function.name
                if func_name in self.available_functions:
                    result = self.available_functions[func_name](
                        **call.function.arguments
                    )
                else:
                    result = f"Unknown tool: {func_name}"
                self.messages.append(
                    {"role": "tool", "tool_name": func_name, "content": str(result)}
                )

    def clear_history(self):
        """Clear conversation history, keeping the system prompt."""
        self.messages = [{"role": "system", "content": SYSTEM_PROMPT}]

What is the agent loop?

The while True loop is what makes this an agent, not just a chatbot. Here's the cycle:

  • Send the full conversation (including tool descriptions) to the Ollama model with stream=True so tokens arrive one by one.
  • As chunks arrive, yield text content to the caller (for real-time streaming) and collect any tool calls the model requests.
  • If the model requested tool calls, execute each function and append the results as "role": "tool" messages. Then loop again so the model can incorporate the results.
  • If there are no tool calls, the model is done. Break out of the loop.

The think=True parameter enables extended thinking, which helps the model make better decisions about when and how to use tools.

  • Save agent/services.py.
  • Verify the file loads without errors by running:
python -c "from agent.services import AgentService; print('Agent service loaded')"

You should see Agent service loaded printed in your terminal.

Seeing an import error?

  • If you see ModuleNotFoundError: No module named 'ollama', make sure your virtual environment is activated. You installed the ollama package there in Step 1.
  • If you see a SyntaxError, check that the indentation inside the class is consistent (4 spaces per level).

Help me fix my agent service import error.

✔️ Awesome, I've got everything!

Great. Make sure you've saved agent/services.py before moving on.

ⓧ I'd like to double check the full code

import json
from ollama import chat


SYSTEM_PROMPT = """You are a helpful AI assistant with access to tools. 
When a user asks a question that requires calculation, use the calculate tool.
When a user asks about a topic that might be in the knowledge base, use the lookup_knowledge tool.
Always use tools when they are relevant to the user's question.
Provide clear, concise answers based on the tool results."""


class AgentService:
    def __init__(self, model: str = "qwen3:8b"):
        self.model = model
        self.messages = [{"role": "system", "content": SYSTEM_PROMPT}]
        self.tools = []
        self.available_functions = {}

    def register_tool(self, func):
        """Register a Python function as a tool the agent can call."""
        self.tools.append(func)
        self.available_functions[func.__name__] = func

    def chat(self, user_message: str):
        """Send a message and run the agent loop. Yields response chunks."""
        self.messages.append({"role": "user", "content": user_message})

        while True:
            response = chat(
                model=self.model,
                messages=self.messages,
                tools=self.tools,
                stream=True,
                think=True,
            )

            content = ""
            tool_calls = []

            for chunk in response:
                if chunk.message.content:
                    content += chunk.message.content
                    yield chunk.message.content
                if chunk.message.tool_calls:
                    tool_calls.extend(chunk.message.tool_calls)

            if content or tool_calls:
                assistant_message = {"role": "assistant", "content": content}
                if tool_calls:
                    assistant_message["tool_calls"] = tool_calls
                self.messages.append(assistant_message)

            if not tool_calls:
                break

            for call in tool_calls:
                func_name = call.function.name
                if func_name in self.available_functions:
                    result = self.available_functions[func_name](
                        **call.function.arguments
                    )
                else:
                    result = f"Unknown tool: {func_name}"
                self.messages.append(
                    {"role": "tool", "tool_name": func_name, "content": str(result)}
                )

    def clear_history(self):
        """Clear conversation history, keeping the system prompt."""
        self.messages = [{"role": "system", "content": SYSTEM_PROMPT}]
Define the tool functions

Tools are regular Python functions that the AI can decide to call. The Ollama SDK reads each function's type hints and docstring to generate the schema it sends to the model.

What is tool calling?

Tool calling lets an LLM request the execution of external functions. The model receives descriptions of available tools, decides which one fits the user's query, and returns structured function calls with arguments. Your application executes the function and feeds the result back to the model.

  • Create a new file called agent/tools.py and add the calculate function:
import json


def calculate(expression: str) -> str:
    """
    Evaluate a mathematical expression and return the result.

    Args:
        expression: A mathematical expression to evaluate (e.g., '1234 * 5678')

    Returns:
        The result of the calculation as a string
    """
    try:
        # Only allow safe characters for math expressions
        allowed_chars = set("0123456789+-*/().% ")
        if not all(c in allowed_chars for c in expression):
            return "Error: Expression contains invalid characters. Only numbers and basic operators (+, -, *, /, %, .) are allowed."
        result = eval(expression)
        return str(result)
    except Exception as e:
        return f"Error: Could not evaluate expression. {str(e)}"

What does this code do?

  • The type hint expression: str and the docstring tell Ollama what argument this tool expects and what it does. The model uses this to decide when to call it.
  • allowed_chars restricts input to safe math characters before calling eval(). This prevents code injection.
  • The function returns a string because all tool results get passed back to the model as text.

Now add the knowledge base and its search function below the calculate function.

  • Add the following code at the bottom of agent/tools.py:
KNOWLEDGE_BASE = {
    "django": "Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. It was created in 2003 and is maintained by the Django Software Foundation. Key features include an ORM, URL routing, template engine, and admin interface.",
    "ollama": "Ollama is an open-source tool for running large language models locally on your own computer. It supports models like Llama, Qwen, Gemma, and Mistral. It provides a REST API on localhost:11434 and has official Python and JavaScript libraries.",
    "ai agent": "An AI agent is a system that uses a language model to decide what actions to take. Unlike simple chatbots, agents can call tools (functions), access databases, make API calls, and take multi-step actions to accomplish goals. The agent loop involves: receiving input, deciding if tools are needed, calling tools, and generating a final response.",
    "tool calling": "Tool calling (or function calling) allows an LLM to request the execution of external functions. The model receives descriptions of available tools, decides which to use based on the user's query, and returns structured function calls with arguments. The application executes the functions and feeds results back to the model.",
    "python": "Python is a high-level, interpreted programming language known for its readability and versatility. It is widely used in web development, data science, AI/ML, automation, and scripting. Python 3.12+ is the current recommended version.",
}


def lookup_knowledge(topic: str) -> str:
    """
    Search a knowledge base for information about a topic.

    Args:
        topic: The topic to search for in the knowledge base

    Returns:
        Information about the topic if found, or a message indicating no results
    """
    topic_lower = topic.lower()
    results = []
    for key, value in KNOWLEDGE_BASE.items():
        if topic_lower in key or key in topic_lower:
            results.append(value)
    if results:
        return " ".join(results)
    return f"No information found about '{topic}' in the knowledge base."

What does this code do?

  • KNOWLEDGE_BASE is a simple dictionary that maps topics to descriptions. In a production app this could be a database or vector store, but a dictionary keeps things focused on the agent pattern.
  • lookup_knowledge does a case-insensitive substring match against the dictionary keys. If the user asks about "Django", it finds the "django" entry.
  • The docstring format matters. Ollama parses it to generate the tool schema that tells the model what this function does and what arguments it needs.
  • Save agent/tools.py.
  • Test that the calculator tool works by running:
python -c "from agent.tools import calculate; print(calculate('2 + 2'))"

You should see 4 printed in your terminal.

Seeing a ModuleNotFoundError?

  • Make sure you're running the command from your project's root folder (the one containing manage.py).
  • Check that agent/tools.py is saved inside the agent directory (not in myproject).

Help me fix my tools import error.

✔️ Awesome, I've got everything!

Great. Make sure you've saved agent/tools.py before moving on.

ⓧ I'd like to double check the full code

import json


def calculate(expression: str) -> str:
    """
    Evaluate a mathematical expression and return the result.

    Args:
        expression: A mathematical expression to evaluate (e.g., '1234 * 5678')

    Returns:
        The result of the calculation as a string
    """
    try:
        allowed_chars = set("0123456789+-*/().% ")
        if not all(c in allowed_chars for c in expression):
            return "Error: Expression contains invalid characters. Only numbers and basic operators (+, -, *, /, %, .) are allowed."
        result = eval(expression)
        return str(result)
    except Exception as e:
        return f"Error: Could not evaluate expression. {str(e)}"


KNOWLEDGE_BASE = {
    "django": "Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. It was created in 2003 and is maintained by the Django Software Foundation. Key features include an ORM, URL routing, template engine, and admin interface.",
    "ollama": "Ollama is an open-source tool for running large language models locally on your own computer. It supports models like Llama, Qwen, Gemma, and Mistral. It provides a REST API on localhost:11434 and has official Python and JavaScript libraries.",
    "ai agent": "An AI agent is a system that uses a language model to decide what actions to take. Unlike simple chatbots, agents can call tools (functions), access databases, make API calls, and take multi-step actions to accomplish goals. The agent loop involves: receiving input, deciding if tools are needed, calling tools, and generating a final response.",
    "tool calling": "Tool calling (or function calling) allows an LLM to request the execution of external functions. The model receives descriptions of available tools, decides which to use based on the user's query, and returns structured function calls with arguments. The application executes the functions and feeds results back to the model.",
    "python": "Python is a high-level, interpreted programming language known for its readability and versatility. It is widely used in web development, data science, AI/ML, automation, and scripting. Python 3.12+ is the current recommended version.",
}


def lookup_knowledge(topic: str) -> str:
    """
    Search a knowledge base for information about a topic.

    Args:
        topic: The topic to search for in the knowledge base

    Returns:
        Information about the topic if found, or a message indicating no results
    """
    topic_lower = topic.lower()
    results = []
    for key, value in KNOWLEDGE_BASE.items():
        if topic_lower in key or key in topic_lower:
            results.append(value)
    if results:
        return " ".join(results)
    return f"No information found about '{topic}' in the knowledge base."

Your agent's brain and its tools are ready. Next up, you'll create the Django view that connects incoming HTTP requests to this agent service and streams the AI's response back in real-time.

Create the Chat API View

You have an agent service that can call tools and stream responses. Now you need a way for the outside world to talk to it.

In this step, you will create a Django view that accepts user messages over HTTP, passes them to the agent, and streams the AI's response back token-by-token using StreamingHttpResponse. You will also wire up URL routing and verify everything works with a quick curl command.

In this step, get ready to:
  • Create a Django view that streams AI responses back to the client.
  • Wire up URL routing so the chat endpoint is reachable.
  • Test the endpoint with curl to confirm the agent responds and calls tools.
Create the chat view with streaming

Django views are Python functions that receive an HTTP request and return an HTTP response. For our AI agent, we need a special kind of response that sends data as it becomes available instead of waiting for the entire reply to finish.

  • Create a new file called views.py inside your agent folder.
  • Add the imports and session management code by pasting the following:
import json

from django.http import JsonResponse, StreamingHttpResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods

from .services import AgentService
from .tools import calculate, lookup_knowledge

# Store agent instances per session (simple in-memory approach for dev)
agents = {}


def get_agent(session_id: str) -> AgentService:
    """Get or create an agent for a session."""
    if session_id not in agents:
        agent = AgentService()
        agent.register_tool(calculate)
        agent.register_tool(lookup_knowledge)
        agents[session_id] = agent
    return agents[session_id]


def home(request):
    """Serve the chat interface."""
    return render(request, "agent/chat.html")

What does this code do?

The agents dictionary stores one AgentService instance per user session. This means each user gets their own conversation history.

The get_agent function either retrieves an existing agent or creates a fresh one with both tools registered. The home view is a placeholder that will serve the chat interface HTML in the next step.

  • Below the home function, add the main chat endpoint:
@csrf_exempt
@require_http_methods(["POST"])
def chat_view(request):
    """Handle chat messages and stream AI responses."""
    try:
        body = json.loads(request.body)
        user_message = body.get("message", "").strip()
    except (json.JSONDecodeError, AttributeError):
        return JsonResponse({"error": "Invalid JSON body"}, status=400)

    if not user_message:
        return JsonResponse({"error": "Message cannot be empty"}, status=400)

    session_id = request.session.session_key
    if not session_id:
        request.session.create()
        session_id = request.session.session_key

    agent = get_agent(session_id)

    def stream_response():
        for chunk in agent.chat(user_message):
            yield chunk

    return StreamingHttpResponse(
        stream_response(),
        content_type="text/plain; charset=utf-8",
    )

What does this code do?

The @csrf_exempt decorator skips CSRF token validation since we are sending JSON from JavaScript, not a traditional HTML form. The @require_http_methods(["POST"]) decorator rejects anything that is not a POST request.

The view parses the JSON body, gets or creates a session, then calls agent.chat() inside a generator function. StreamingHttpResponse iterates over that generator and sends each text chunk to the client as soon as it arrives from Ollama.

  • Below chat_view, add the clear chat endpoint:
@csrf_exempt
@require_http_methods(["POST"])
def clear_chat(request):
    """Clear the conversation history."""
    session_id = request.session.session_key
    if session_id and session_id in agents:
        agents[session_id].clear_history()
    return JsonResponse({"status": "cleared"})

What does this code do?

This endpoint resets the conversation. It finds the agent for the current session and calls clear_history(), which wipes all messages except the system prompt. The user can start a fresh conversation without restarting the server.

  • Save agent/views.py.

Seeing import errors?

Make sure your virtual environment is activated. Also check that both agent/services.py and agent/tools.py exist from the previous step.

help me fix import errors in my Django views file

✔️ Awesome, I've got everything!

Great. Double check you have saved agent/views.py before moving on.

ⓧ I'd like to double check the full code

import json

from django.http import JsonResponse, StreamingHttpResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods

from .services import AgentService
from .tools import calculate, lookup_knowledge

# Store agent instances per session (simple in-memory approach for dev)
agents = {}


def get_agent(session_id: str) -> AgentService:
    """Get or create an agent for a session."""
    if session_id not in agents:
        agent = AgentService()
        agent.register_tool(calculate)
        agent.register_tool(lookup_knowledge)
        agents[session_id] = agent
    return agents[session_id]


def home(request):
    """Serve the chat interface."""
    return render(request, "agent/chat.html")


@csrf_exempt
@require_http_methods(["POST"])
def chat_view(request):
    """Handle chat messages and stream AI responses."""
    try:
        body = json.loads(request.body)
        user_message = body.get("message", "").strip()
    except (json.JSONDecodeError, AttributeError):
        return JsonResponse({"error": "Invalid JSON body"}, status=400)

    if not user_message:
        return JsonResponse({"error": "Message cannot be empty"}, status=400)

    session_id = request.session.session_key
    if not session_id:
        request.session.create()
        session_id = request.session.session_key

    agent = get_agent(session_id)

    def stream_response():
        for chunk in agent.chat(user_message):
            yield chunk

    return StreamingHttpResponse(
        stream_response(),
        content_type="text/plain; charset=utf-8",
    )


@csrf_exempt
@require_http_methods(["POST"])
def clear_chat(request):
    """Clear the conversation history."""
    session_id = request.session.session_key
    if session_id and session_id in agents:
        agents[session_id].clear_history()
    return JsonResponse({"status": "cleared"})
Wire up the URL configuration

Django uses URL patterns to map incoming requests to the correct view function. Your project-level myproject/urls.py already includes the agent app's URLs. Now you need to create the app-level URL file that defines the actual routes.

  • Create a new file called urls.py inside your agent folder.
  • Add the URL patterns by pasting the following:
from django.urls import path
from . import views

urlpatterns = [
    path("", views.home, name="home"),
    path("chat/", views.chat_view, name="chat"),
    path("clear/", views.clear_chat, name="clear_chat"),
]

What does this code do?

Each path() call maps a URL to a view function. The empty string "" maps the root URL to the home page. "chat/" handles incoming messages. "clear/" resets the conversation.

  • Save agent/urls.py.

Getting a ModuleNotFoundError?

Make sure the file is named exactly urls.py (not url.py) and is saved inside the agent folder alongside views.py.

help me fix a ModuleNotFoundError for agent.urls in my Django project

Test the endpoint with curl

Before building the frontend, you can verify the entire pipeline works using curl from your terminal. First, you need to run database migrations so Django can store session data.

  • Apply Django's built-in migrations by running:
python manage.py migrate
  • Start the Django development server by running:
python manage.py runserver

You should see output indicating the server is running at http://127.0.0.1:8000/.

  • Open a second terminal window (keep the server running in the first one).
  • Activate your virtual environment in the new terminal.
  • Test the chat endpoint by running this curl command:
curl -X POST http://localhost:8000/chat/ -H "Content-Type: application/json" -d "{\"message\": \"What is 1234 * 5678?\"}"

You should see the AI's response stream into your terminal. The agent will use the calculator tool and return the result (7006652).

Why does the response take a moment?

The first request to Ollama may be slow because the model needs to load into memory. Subsequent requests will be much faster. If you are running on CPU without a dedicated GPU, responses will take longer than on a machine with a GPU.

Getting a connection refused error?

Make sure the Django development server is still running in your first terminal. Also verify that Ollama is running by opening a new terminal and running ollama list. You should see qwen3:8b in the output.

help me debug why my curl request to the Django chat endpoint is failing

Your API is live and the agent is calling tools. Next up, you will build a proper chat interface so you can talk to your agent in the browser.

Build the Chat Interface

Your agent's backend is fully wired. It can receive messages, call tools, and stream responses chunk by chunk. But right now, the only way to talk to it is through curl commands in the terminal.

In this step, you'll build an HTML chat interface that connects to your streaming endpoint. The page will display the AI's response token by token as it generates them, just like ChatGPT.

In this step, get ready to:
  • Create the Django template directory structure and HTML/CSS chat layout.
  • Add JavaScript that streams AI responses in real-time using the Fetch API.
  • Verify the chat interface loads in your browser.
Create the template directory and chat layout

Django looks for templates in a specific folder structure. For an app called agent, it expects templates inside agent/templates/agent/.

  • Create the template directory by running these commands:
mkdir -p agent/templates/agent

Why the double agent/ folder?

Django's template loader searches across all apps. The nested agent/templates/agent/ structure namespaces your templates so they don't collide with templates from other apps that might share a filename like chat.html.

  • Create a new file called chat.html inside agent/templates/agent/ and paste the following HTML and CSS:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Agent Chat</title>
    <style>
        /* Reset default browser styles */
        * { margin: 0; padding: 0; box-sizing: border-box; }
        /* Full-height dark layout */
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #1a1a2e; color: #eee; height: 100vh; display: flex; flex-direction: column; }
        /* Top bar with title and clear button */
        .header { padding: 16px 24px; background-color: #16213e; border-bottom: 1px solid #0f3460; display: flex; justify-content: space-between; align-items: center; }
        .header h1 { font-size: 1.2rem; color: #e94560; }
        .clear-btn { padding: 8px 16px; background-color: #0f3460; color: #eee; border: 1px solid #e94560; border-radius: 6px; cursor: pointer; font-size: 0.85rem; }
        .clear-btn:hover { background-color: #e94560; }
        /* Scrollable message area */
        .chat-messages { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 16px; }
        /* Individual message bubbles */
        .message { max-width: 70%; padding: 12px 16px; border-radius: 12px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; }
        .message.user { align-self: flex-end; background-color: #0f3460; color: #eee; }
        .message.assistant { align-self: flex-start; background-color: #16213e; border: 1px solid #0f3460; color: #ddd; }
        /* Input area at the bottom */
        .input-area { padding: 16px 24px; background-color: #16213e; border-top: 1px solid #0f3460; display: flex; gap: 12px; }
        .input-area input { flex: 1; padding: 12px 16px; border-radius: 8px; border: 1px solid #0f3460; background-color: #1a1a2e; color: #eee; font-size: 1rem; outline: none; }
        .input-area input:focus { border-color: #e94560; }
        .input-area button { padding: 12px 24px; background-color: #e94560; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 1rem; font-weight: 600; }
        .input-area button:hover { background-color: #d63851; }
        .input-area button:disabled { background-color: #555; cursor: not-allowed; }
    </style>
</head>
<body>
    <!-- Header with app title and clear button -->
    <div class="header">
        <h1>🤖 AI Agent (Ollama + Django)</h1>
        <button class="clear-btn" onclick="clearChat()">Clear Chat</button>
    </div>
    <!-- Scrollable area where messages appear -->
    <div class="chat-messages" id="chatMessages">
        <div class="message assistant">Hello! I'm your AI agent. I can do calculations and look up information. Try asking me something!</div>
    </div>
    <!-- Text input and send button -->
    <div class="input-area">
        <input type="text" id="messageInput" placeholder="Type your message..." onkeypress="if(event.key==='Enter')sendMessage()">
        <button id="sendBtn" onclick="sendMessage()">Send</button>
    </div>
    <script>

    </script>
</body>
</html>

What does this HTML do?

  • The .header section shows the app title and a **Clear Chat** button.
  • The .chat-messages div is a scrollable area where user and assistant messages will stack up.
  • The .input-area at the bottom contains the text input and send button. Pressing Enter also triggers sending.
  • The CSS uses flexbox to create a full-height layout. User messages align right, assistant messages align left.
  • Save chat.html.

The template exists but the <script> tag is empty. The chat layout will render, but nothing happens when you type a message. The next substep adds the streaming logic.

Add JavaScript for real-time streaming

The JavaScript needs to send the user's message to your /chat/ endpoint, then read the streamed response chunk by chunk using the Fetch API's ReadableStream reader.

  • In chat.html, find the empty <script> tag near the bottom of the file.
  • Replace the empty <script> block with the following:
    <script>
        // Cache DOM references for the message area, input field, and send button
        const chatMessages = document.getElementById('chatMessages');
        const messageInput = document.getElementById('messageInput');
        const sendBtn = document.getElementById('sendBtn');

        // Helper to create and append a message bubble
        function addMessage(content, role) {
            const div = document.createElement('div');
            div.className = `message ${role}`;
            div.textContent = content;
            chatMessages.appendChild(div);
            chatMessages.scrollTop = chatMessages.scrollHeight;
            return div;
        }

        // Send user message and stream the AI response
        async function sendMessage() {
            const message = messageInput.value.trim();
            if (!message) return;
            messageInput.value = '';
            sendBtn.disabled = true;
            addMessage(message, 'user');
            // Create an empty assistant bubble to fill as chunks arrive
            const assistantDiv = addMessage('', 'assistant');
            try {
                const response = await fetch('/chat/', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ message: message }),
                });
                if (!response.ok) {
                    assistantDiv.textContent = 'Error: Failed to get response.';
                    sendBtn.disabled = false;
                    return;
                }
                // Read the streamed response chunk by chunk
                const reader = response.body.getReader();
                const decoder = new TextDecoder();
                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;
                    const text = decoder.decode(value, { stream: true });
                    assistantDiv.textContent += text;
                    chatMessages.scrollTop = chatMessages.scrollHeight;
                }
            } catch (error) {
                assistantDiv.textContent = 'Error: Could not connect to the server.';
            }
            sendBtn.disabled = false;
            messageInput.focus();
        }

        // Clear conversation on both client and server
        async function clearChat() {
            await fetch('/clear/', { method: 'POST' });
            chatMessages.innerHTML = '';
            addMessage("Chat cleared! How can I help you?", 'assistant');
        }

        // Auto-focus the input on page load
        messageInput.focus();
    </script>

How does the streaming work?

The key is response.body.getReader(). Instead of waiting for the entire response to finish, this gives you a ReadableStream reader that lets you consume bytes as they arrive from the server.

Each time reader.read() resolves, you get a chunk of text. The code appends each chunk to the assistant's message bubble immediately. This creates the "typing" effect where users see words appear in real-time.

  • Save chat.html.

Seeing a syntax error?

  • Make sure the <script> opening and closing tags are present. The JavaScript must sit between <script> and </script>.
  • Check for mismatched backticks in the template literal `message ${role}`. These are backticks, not regular quotes.

Help me debug my chat.html JavaScript

Verify the chat interface loads

The home view in agent/views.py already renders agent/chat.html, and the URL pattern at / already points to it. Everything is wired from the previous step.

  • Start the Django development server by running:
python manage.py runserver
  • Open your browser and go to http://127.0.0.1:8000/.

You should see a dark-themed chat interface with a header that says "AI Agent (Ollama + Django)", a welcome message from the assistant, and a text input at the bottom.

Seeing a TemplateDoesNotExist error?

  • Double-check that your file is at exactly agent/templates/agent/chat.html. The path must include both agent/ folders.
  • Confirm that "agent" is listed in INSTALLED_APPS in myproject/settings.py. Django only searches templates in registered apps.
  • Make sure APP_DIRS is set to True in the TEMPLATES setting. This tells Django to look inside each app's templates/ folder.

Help me fix my TemplateDoesNotExist error

Why not test sending a message yet?

The interface will render, but sending a message requires Ollama to be running with the qwen3:8b model loaded. The next step covers end-to-end testing with real prompts that trigger tool calls.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete chat.html file for comparison:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Agent Chat</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #1a1a2e; color: #eee; height: 100vh; display: flex; flex-direction: column; }
        .header { padding: 16px 24px; background-color: #16213e; border-bottom: 1px solid #0f3460; display: flex; justify-content: space-between; align-items: center; }
        .header h1 { font-size: 1.2rem; color: #e94560; }
        .clear-btn { padding: 8px 16px; background-color: #0f3460; color: #eee; border: 1px solid #e94560; border-radius: 6px; cursor: pointer; font-size: 0.85rem; }
        .clear-btn:hover { background-color: #e94560; }
        .chat-messages { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 16px; }
        .message { max-width: 70%; padding: 12px 16px; border-radius: 12px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; }
        .message.user { align-self: flex-end; background-color: #0f3460; color: #eee; }
        .message.assistant { align-self: flex-start; background-color: #16213e; border: 1px solid #0f3460; color: #ddd; }
        .input-area { padding: 16px 24px; background-color: #16213e; border-top: 1px solid #0f3460; display: flex; gap: 12px; }
        .input-area input { flex: 1; padding: 12px 16px; border-radius: 8px; border: 1px solid #0f3460; background-color: #1a1a2e; color: #eee; font-size: 1rem; outline: none; }
        .input-area input:focus { border-color: #e94560; }
        .input-area button { padding: 12px 24px; background-color: #e94560; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 1rem; font-weight: 600; }
        .input-area button:hover { background-color: #d63851; }
        .input-area button:disabled { background-color: #555; cursor: not-allowed; }
    </style>
</head>
<body>
    <div class="header">
        <h1>🤖 AI Agent (Ollama + Django)</h1>
        <button class="clear-btn" onclick="clearChat()">Clear Chat</button>
    </div>
    <div class="chat-messages" id="chatMessages">
        <div class="message assistant">Hello! I'm your AI agent. I can do calculations and look up information. Try asking me something!</div>
    </div>
    <div class="input-area">
        <input type="text" id="messageInput" placeholder="Type your message..." onkeypress="if(event.key==='Enter')sendMessage()">
        <button id="sendBtn" onclick="sendMessage()">Send</button>
    </div>
    <script>
        const chatMessages = document.getElementById('chatMessages');
        const messageInput = document.getElementById('messageInput');
        const sendBtn = document.getElementById('sendBtn');
        function addMessage(content, role) {
            const div = document.createElement('div');
            div.className = `message ${role}`;
            div.textContent = content;
            chatMessages.appendChild(div);
            chatMessages.scrollTop = chatMessages.scrollHeight;
            return div;
        }
        async function sendMessage() {
            const message = messageInput.value.trim();
            if (!message) return;
            messageInput.value = '';
            sendBtn.disabled = true;
            addMessage(message, 'user');
            const assistantDiv = addMessage('', 'assistant');
            try {
                const response = await fetch('/chat/', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ message: message }),
                });
                if (!response.ok) {
                    assistantDiv.textContent = 'Error: Failed to get response.';
                    sendBtn.disabled = false;
                    return;
                }
                const reader = response.body.getReader();
                const decoder = new TextDecoder();
                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;
                    const text = decoder.decode(value, { stream: true });
                    assistantDiv.textContent += text;
                    chatMessages.scrollTop = chatMessages.scrollHeight;
                }
            } catch (error) {
                assistantDiv.textContent = 'Error: Could not connect to the server.';
            }
            sendBtn.disabled = false;
            messageInput.focus();
        }
        async function clearChat() {
            await fetch('/clear/', { method: 'POST' });
            chatMessages.innerHTML = '';
            addMessage("Chat cleared! How can I help you?", 'assistant');
        }
        messageInput.focus();
    </script>
</body>
</html>

Your chat interface is ready. Next up, you'll start the full system and test your AI agent with real prompts that trigger tool calls.

Test Your AI Agent

You've built all the pieces of your AI agent: the tools, the agent service, the streaming view, and the chat interface. Now it's time to bring it all together and see your agent in action.

In this step, you'll start the Django development server and put your agent through its paces. You'll test whether it can call the right tools, return accurate results, and remember what you said earlier in the conversation.

In this step, get ready to:
  • Start the Django development server and open the chat interface.
  • Test tool calling with the calculator and knowledge base tools.
  • Verify multi-turn conversation history is working.
Start the Django development server

Before the server can start, Django needs its database tables set up. Django uses sessions to track each user's conversation history, and sessions are stored in the database.

  • Make sure your virtual environment is still active in your terminal. You should see (venv) at the start of your prompt.
  • Create the database tables by running this command:
python manage.py migrate

You should see a series of lines starting with Applying followed by OK for each migration. This creates the session tables your agent needs.

Seeing an error about missing modules?

Make sure your virtual environment is activated. If you see an error mentioning Django is not installed, activate it again with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).

Help me fix my Django migration error.

  • Start the development server by running:
python manage.py runserver

You should see output indicating the server is running at http://127.0.0.1:8000/. Keep this terminal running.

  • Open your web browser and navigate to http://127.0.0.1:8000/.

You should see the chat interface with a dark background, a red header reading "AI Agent (Ollama + Django)", and a welcome message from the assistant.

Seeing a blank page or error?

Make sure Ollama is running in the background. Open a separate terminal and run ollama list to confirm it's active and the qwen3:8b model is available.

If you see a "Template not found" error, check that your chat.html file is at the correct path: agent/templates/agent/chat.html.

Help me fix my Django server error.

Test tool calling with the calculator and knowledge base

The real test of an AI agent is whether it knows WHEN to use its tools. A simple chatbot would try to answer math questions from memory (and get them wrong). Your agent should recognize it needs the calculator tool and call it automatically.

  • In the chat input field at the bottom of the page, type the following and press **Enter** or click **Send**:
What is 1234 * 5678?

Watch the response stream in token by token. The agent should return 7006652 as the result. This confirms the calculate tool was called behind the scenes.

What's happening behind the scenes?

When you send that message, the agent loop in services.py kicks in. The model sees your question, recognizes it's a math problem, and returns a tool calling request for the calculate function with the expression 1234 * 5678.

Your code executes that function, feeds the result back to the model, and the model then generates a natural language response incorporating the answer. All of this happens in one streaming loop.

  • Now test the knowledge base tool. Type this into the chat:
Tell me about Django

The agent should respond with information from your knowledge base, mentioning that Django is a high-level Python web framework created in 2003. This confirms the lookup_knowledge tool is working.

Why does the response take a few seconds?

The qwen3:8b model runs locally on your machine. Response speed depends on your hardware. With a dedicated GPU, responses are fast. On CPU only, expect a longer wait of 10-30 seconds per response.

Agent not using tools or giving wrong answers?

If the agent gives a generic answer without calling tools, check that your SYSTEM_PROMPT in agent/services.py tells the model to use the tools. Also verify that agent/views.py registers both calculate and lookup_knowledge with agent.register_tool().

If the response never appears, check your terminal running the Django server for error messages. A common issue is the Ollama service not running.

Help me debug my AI agent's tool calling.

Test multi-turn conversation history

A key feature of your agent is conversation memory. The AgentService class stores every message in self.messages, so the model can reference things you said earlier. Without this, every message would feel like a fresh conversation with a stranger.

  • Type this message into the chat to set up context:
What is 100 + 250?
  • Wait for the agent to respond with 350. Then type a follow-up that references the previous answer:
Now multiply that result by 3

The agent should understand that "that result" refers to 350 and return 1050. This proves your conversation history is working correctly.

How does conversation memory work here?

Every time you send a message, it gets appended to self.messages in the AgentService class. The entire conversation history (including tool calls and results) is sent to the model on each turn. The model uses this context to understand follow-up questions.

  • Test the **Clear Chat** button in the top right corner of the interface.
  • After clearing, type a follow-up like "What was my last question?" to confirm the agent no longer remembers your previous messages.

The agent should not know what your last question was. This confirms the clear_history() method is resetting the conversation properly.

Agent still remembers after clearing?

Check that the clear_chat view in agent/views.py is calling agents[session_id].clear_history(). Also check your browser's developer console (press F12) for any network errors on the /clear/ endpoint.

Help me fix conversation clearing in my Django AI agent.

Your AI agent is fully operational. It can call tools autonomously, stream responses in real-time, and maintain conversation context across multiple turns. Next up in the Secret Mission, you'll give your agent the power to create structured task plans and store them in a database.

Secret mission

Add a Task Planner Tool

Your agent can calculate and look things up. But what if it could also plan? In this secret mission, you'll give your AI agent the ability to break down complex goals into step-by-step plans and persist them to a database using a Django model.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Ollama with the qwen3:8b model (uses disk space for the downloaded model).
  • The myproject Django project folder on your Desktop (includes virtual environment, database, and all code).

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment with more tools and models.

  • Ollama stays running in the background and serves the model when your Django app requests it.
  • Your virtual environment and project folder stay on your Desktop.
  • The qwen3:8b model stays downloaded (approximately 8 GB of disk space).

Pause - I'll come back to this later

Shut down running processes to free up memory. Your project files and model stay on disk for when you return.

  • Stop the Django development server by pressing Ctrl+C in the terminal where it is running.
  • Deactivate your virtual environment by running:
deactivate
  • Ollama continues running as a background service. To resume later, navigate back into your myproject folder, reactivate your virtual environment, and start the server again.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each item below to fully clean up.

  • Stop the Django development server by pressing Ctrl+C in the terminal where it is running.
  • Deactivate your virtual environment by running:
deactivate
  • Remove the downloaded Ollama model by running:
ollama rm qwen3:8b
  • Delete the project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf myproject
  • On Windows, run:
cd ~\Desktop
rmdir /s /q myproject
  • To uninstall Ollama entirely on Windows, open Settings, select Apps, then Installed apps. Find Ollama and click Uninstall.
  • To uninstall Ollama entirely on macOS, run:
sudo rm -rf /Applications/Ollama.app
sudo rm /usr/local/bin/ollama
rm -rf ~/.ollama
  • On Windows, also delete the Ollama data folder by running this in PowerShell:
rm -Recurse -Force "$env:USERPROFILE\.ollama"

Nice Work!

Nice Work!

You just built a fully functional AI agent that runs entirely on your local machine. No API keys, no cloud costs, no data leaving your computer.

You've learned how to:

  • Built a Django web application with StreamingHttpResponse that streams AI-generated text token-by-token to a real-time chat interface.
  • Created an AI agent service with an agent loop that detects tool calls, executes Python functions, and feeds results back to the model until it produces a final answer.
  • Defined custom tools (a calculator and a knowledge base lookup) that the AI invokes autonomously based on the user's question.
  • Added a task planner tool backed by a Django model that saves structured step-by-step plans to the database.

Ready to quiz yourself?