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.

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

bash
curl -fsSL https://ollama.com/install.sh | sh
ollama pull llama3.1
ollama run llama3.1   # interactive test

A Python chat endpoint

python
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

jsx
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.

python
@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.

Your move

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 →