Build a Local Chatbot with Ollama, Python & React
Run an LLM entirely on your own machine — no cloud, no API keys, no per-token billing — with a chat UI on top.
In this guide
You can run a capable language model on a normal laptop today. Ollama makes it a one-liner, and a thin Python bridge gives you a private chatbot that never leaves your network.
Install and run a model
curl -fsSL https://ollama.com/install.sh | sh
ollama pull llama3.1
ollama run llama3.1 # interactive testA Python chat endpoint
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import httpx, json
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
@app.post("/chat")
async def chat(prompt: str):
async with httpx.AsyncClient() as c:
r = await c.post(
"http://localhost:11434/api/generate",
json={"model": "llama3.1", "prompt": prompt, "stream": False},
timeout=120,
)
return json.loads(r.text)["response"]A React chat UI
import { useState } from "react";
export default function Chat() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
async function send() {
const q = input; setInput("");
setMessages((m) => [...m, { role: "user", text: q }]);
const r = await fetch("http://localhost:8000/chat?prompt=" + encodeURIComponent(q));
const text = await r.json();
setMessages((m) => [...m, { role: "assistant", text }]);
}
return (
<div>
<div>{messages.map((m, i) => <p key={i}><b>{m.role}:</b> {m.text}</p>)}</div>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={send}>Send</button>
</div>
);
}Stream it for a better feel
Non-streaming feels laggy for long answers. Set stream: true and read newline-delimited JSON chunks, forwarding each token to the client as it arrives.
@app.post("/chat-stream")
async def chat_stream(prompt: str):
from fastapi.responses import StreamingResponse
async def gen():
async with httpx.AsyncClient() as c:
async with c.stream("POST", "http://localhost:11434/api/generate",
json={"model": "llama3.1", "prompt": prompt}) as r:
async for line in r.aiter_lines():
if line:
token = json.loads(line).get("response", "")
yield token
return StreamingResponse(gen(), media_type="text/event-stream")Privacy is the point. Local models keep sensitive prompts on your hardware — which is why we build these for clients with NDAs and regulated data.
From here, add a vector store for retrieval, tool calls, and multiple models. You've just built the core of a private AI assistant.
More from the lab
Model Context Protocol (MCP): Give Your AI Real Tools
MCP lets AI call your tools, files and APIs. Here's what it is and how to expose one in a dozen lines.
AI & MLClassifiers, Regressors & Clusters: The 3 Shapes of ML
Almost every ML problem is one of three shapes — here's how to recognise and solve each with scikit-learn.
PythonAdvanced Python Data Structures for Interviews & Code
collections, defaultdicts, heaps and deques — the structures that show up in every interview and codebase.
Want us to build this for you?
We turn tutorials like these into production systems every day. Tell us what you need.
Start a project →