Build
How to build an AI chatbot with Streamlit and Python.
Streamlit is the fastest way I know to put a working interface in front of a machine learning model, and its built-in chat components make an LLM chatbot almost embarrassingly short to write. In this guide we'll build a complete, streaming chat app — message history, typing-as-it-generates output, proper secrets handling — in under fifty lines of Python.
Setup
You need Python 3.10+ and two packages:
pip install streamlit anthropic
We'll use the Anthropic API as the model behind the chat (any provider with a Python SDK works the same way — and see the end of the post for running a fully local model instead). Create an API key in the Claude developer console and expose it to the app. For local development, an environment variable is fine:
# macOS / Linux
export ANTHROPIC_API_KEY="sk-ant-..."
# Windows (PowerShell)
$env:ANTHROPIC_API_KEY = "sk-ant-..."
For anything you deploy, use Streamlit's secrets mechanism instead: create a file called
.streamlit/secrets.toml (and add it to .gitignore):
ANTHROPIC_API_KEY = "sk-ant-..."
The three Streamlit primitives you need
Streamlit's chat support rests on three pieces:
-
st.chat_input()— a chat text box pinned to the bottom of the page. Returns the submitted string, orNoneif nothing was submitted this run. -
st.chat_message(role)— a message bubble container with an avatar, used as a context manager:with st.chat_message("user"): ... -
st.session_state— a dictionary that survives across reruns. This one is essential: Streamlit re-executes your entire script from top to bottom on every interaction, so any state you don't stash insession_stateis lost the moment the user does anything.
That rerun model is the only conceptually tricky part of Streamlit. Once you internalize
"the script runs fresh every time, state lives in session_state," the rest
is straightforward.
The complete app
Save this as app.py:
import os
import streamlit as st
import anthropic
st.title("💬 Chat")
SYSTEM_PROMPT = "You are a concise, helpful assistant."
# Read the key from Streamlit secrets if present, else the environment
api_key = st.secrets.get("ANTHROPIC_API_KEY", os.environ.get("ANTHROPIC_API_KEY"))
if not api_key:
st.error("No API key found. Set ANTHROPIC_API_KEY in .streamlit/secrets.toml.")
st.stop()
client = anthropic.Anthropic(api_key=api_key)
# 1. Initialize the conversation once per session
if "messages" not in st.session_state:
st.session_state.messages = []
# 2. Re-render the full history on every rerun
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# 3. Handle new input
if prompt := st.chat_input("Ask me anything"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
# 4. Stream the model's reply token by token
with st.chat_message("assistant"):
with client.messages.stream(
model="claude-opus-4-8",
max_tokens=1024,
system=SYSTEM_PROMPT,
messages=st.session_state.messages,
) as stream:
reply = st.write_stream(stream.text_stream)
st.session_state.messages.append({"role": "assistant", "content": reply})
Run it:
streamlit run app.py
Your browser opens at http://localhost:8501 with a working chatbot: full
conversation memory, and responses that stream in as the model generates them.
How it works, line by line
-
Step 1 creates the message list exactly once. On later reruns the
if "messages" not in st.session_stateguard is false, so history survives. - Step 2 redraws every stored message on each rerun. This is idiomatic Streamlit — you re-render the world from state rather than mutating a UI.
- Step 3 uses the walrus operator to both read and test the chat input. The user's message is stored and rendered immediately, so it appears while the model is still thinking.
-
Step 4 is the nice part. The SDK's
stream()helper yields text chunks viastream.text_stream, and Streamlit'sst.write_stream()consumes any generator, rendering chunks as they arrive — and returns the concatenated full text when done, which we store back into history. The whole conversation is sent on every turn because LLM APIs are stateless; the model only "remembers" what you resend.
Useful upgrades
A system prompt selector and a clear button
Streamlit's sidebar makes small control panels trivial:
with st.sidebar:
persona = st.selectbox("Persona", ["Concise assistant", "Socratic tutor", "Code reviewer"])
if st.button("Clear conversation"):
st.session_state.messages = []
st.rerun()
Capping the history
Costs (and context windows) grow with conversation length, since the full history is resent each turn. A simple guard is to keep only the last N messages:
MAX_TURNS = 20 # keep the last 20 messages
history = st.session_state.messages[-MAX_TURNS:]
…and pass history instead of the full list to the API call.
Running a local model instead
If your data can't leave your machine, the same UI works with a locally hosted model.
Install Ollama, pull a model
(ollama pull llama3.1), install the ollama Python package,
and replace step 4 with:
import ollama
with st.chat_message("assistant"):
stream = ollama.chat(
model="llama3.1",
messages=[{"role": "system", "content": SYSTEM_PROMPT}]
+ st.session_state.messages,
stream=True,
)
reply = st.write_stream(chunk["message"]["content"] for chunk in stream)
No API key, no per-token cost, and your conversations stay local — the trade-off is that you need the hardware to run the model at a usable speed. (Local-first LLM interfaces are something of a theme of mine — see LLMLynx.)
Deploying
The lowest-friction option is Streamlit Community Cloud: push the repo to
GitHub, connect it at share.streamlit.io, and add
your API key in the app's Secrets settings (the same TOML format as the local
file). For anything internal or production-grade, run the app in a container behind your
usual reverse proxy — streamlit run app.py --server.port 8501 works the
same anywhere, and pairs naturally with Docker.
Two production notes. First, never commit API keys — keep
secrets.toml out of version control. Second, a public chatbot with your API key
behind it is an open wallet: add authentication or rate limiting before sharing a link
beyond people you trust.
Want a chatbot that knows your documents, your data, or your product? That's retrieval-augmented generation — and it's the kind of system I design and build end to end.
Get in touch