Build a Chatbot in 50 Lines
The most common AI engineer interview question in 2026 is "show me a project." The second most common is "walk me through how you'd build a chatbot." This lesson answers both, and gives you a deployable artifact you can put in your résumé this weekend.
After this lesson, you will be able to:
- Explain why LLMs don't actually remember conversations, and how to fake it with conversation history
- Build a complete chatbot with conversation memory in under 50 lines of Python using the Anthropic SDK
- Add a system prompt to give your chatbot a custom personality and a domain specialty
- Handle the token limit constraint when conversations get long, using sliding window and summary strategies
- Add streaming responses so the chatbot feels fast (the single biggest UX upgrade)
- Deploy the chatbot to a public URL using Streamlit Cloud or Railway in under 20 minutes
Before You Start
#The Core Insight: LLMs Are Stateless
This single insight separates engineers from users. In every interview, when an interviewer asks "how does conversational memory work in your chatbot?" — saying "the LLM remembers" is wrong and immediately downgrades you. Saying "the LLM is stateless; we send the full history each turn and pay the input tokens" is correct and signals you have actually built one.
#The Complete Chatbot
This code calls input(). Python here can't pause to ask you, so type the answers below before running — one value per line, in the order the program asks for them.
"""
chatbot.py — a 50-line conversational chatbot with memory.
Run: python chatbot.py
Env: export ANTHROPIC_API_KEY=sk-ant-...
"""
import os
import anthropic
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
# This list is the entire "memory" of the chatbot.
conversation_history: list[dict] = []
SYSTEM_PROMPT = (
"You are a helpful AI tutor specializing in machine learning. "
"Be concise, friendly, and clear. If a question is ambiguous, ask "
"one clarifying question before answering. Use plain language."
)
def chat(user_message: str) -> str:
"""Send a message and get a response, maintaining conversation history."""
conversation_history.append({"role": "user", "content": user_message})
# Send the ENTIRE conversation history every time.
# Trim old messages if we get too long (keep last 20 exchanges = 40 messages).
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system=SYSTEM_PROMPT,
messages=conversation_history[-40:],
)
assistant_message = response.content[0].text
conversation_history.append({"role": "assistant", "content": assistant_message})
return assistant_message
if __name__ == "__main__":
print("Chatbot ready! Type 'quit' to exit, 'reset' to clear history.\n")
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() == "quit":
print("Goodbye!")
break
if user_input.lower() == "reset":
conversation_history.clear()
print("Conversation reset.\n")
continue
print(f"AI: {chat(user_input)}\n")That's it. 50 lines. A working chatbot with memory, a custom persona, and a safety rail (sliding-window trimming).
#How the Memory Actually Works
First message ("What is Python?"):
You send: [
{"role": "user", "content": "What is Python?"}
]
AI replies: "Python is a programming language..."
history = [
user: "What is Python?",
assistant: "Python is a programming language..."
]
Second message ("How do I install it?"):
You send: [
{"role": "user", "content": "What is Python?"},
{"role": "assistant", "content": "Python is a programming language..."},
{"role": "user", "content": "How do I install it?"} <-- new message
]
AI replies: "To install Python..."
(The AI knows "it" means Python because the full history is in context.)
#Customize the Personality
system parameter sets the chatbot's persona. Try these.# Study buddy
SYSTEM_PROMPT = (
"You are a patient study buddy helping with ML. Always check the "
"user's understanding with one question before moving on to the next topic."
)
# Cooking assistant
SYSTEM_PROMPT = (
"You are an expert chef. Give practical cooking advice. Always suggest "
"substitutions for ingredients the user may not have."
)
# Senior code reviewer
SYSTEM_PROMPT = (
"You are a senior software engineer doing code review. Be direct, "
"specific, and constructive. Flag security, performance, and "
"readability issues separately."
)
# Interview coach
SYSTEM_PROMPT = (
"You are an AI interview coach for ML engineering roles. Ask ONE "
"technical question at a time. After the user answers, give specific "
"feedback (what was strong, what was missing) before the next question."
)The system prompt is the cheapest differentiator. A "ChatGPT clone" with a generic system prompt is forgettable. A "calculus tutor that always uses the Socratic method" is memorable.
#Add Streaming (The Single Biggest UX Upgrade)
A non-streaming chatbot feels broken in 2026. Users expect tokens to appear word-by-word, ChatGPT-style. The fix is one method call away.
def chat_stream(user_message: str) -> str:
"""Send a message and stream the response token-by-token."""
conversation_history.append({"role": "user", "content": user_message})
full_response = ""
with client.messages.stream(
model="claude-opus-4-5",
max_tokens=1024,
system=SYSTEM_PROMPT,
messages=conversation_history[-40:],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
full_response += text
print() # newline at end
conversation_history.append({"role": "assistant", "content": full_response})
return full_responseThat's the entire change. The user now sees "P-y-t-h-o-n i-s..." appearing live instead of waiting 4 seconds for the full response. Perceived latency drops 60-80%; user retention on free-tier products jumps 20-30%.
#Deploy in 20 Minutes: Streamlit + Streamlit Cloud
For your portfolio, you want a web UI on a public URL, not a CLI script. Streamlit is the fastest path.
# app.py — Streamlit web chatbot. Save next to chatbot.py.
import streamlit as st
import anthropic
import os
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
st.set_page_config(page_title="ML Tutor", page_icon="🤖")
st.title("ML Tutor — built on Claude")
if "messages" not in st.session_state:
st.session_state.messages = []
# Render history
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
# Take a new message
if prompt := st.chat_input("Ask anything about ML..."):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
placeholder = st.empty()
full = ""
with client.messages.stream(
model="claude-opus-4-5",
max_tokens=1024,
system="You are a helpful ML tutor. Be concise and clear.",
messages=st.session_state.messages[-40:],
) as stream:
for text in stream.text_stream:
full += text
placeholder.markdown(full + "▌")
placeholder.markdown(full)
st.session_state.messages.append({"role": "assistant", "content": full})Deploy steps (literal, copy-pasteable):
# 1) Save as app.py and add requirements.txt
echo "streamlit\nanthropic" > requirements.txt
# 2) Push to GitHub
git init && git add . && git commit -m "ML tutor chatbot v1"
gh repo create ml-tutor --public --source=. --push
# 3) Go to https://share.streamlit.io
# Connect GitHub. Pick the repo. Set ANTHROPIC_API_KEY in Secrets.
# Click Deploy.
# 4) Within 60 seconds you have a public URL like:
# https://yourhandle-ml-tutor.streamlit.app
git init to live URL: 15-20 minutes. The deploy is free on Streamlit Cloud (1 app, unlimited time, 1GB RAM). For higher-traffic, switch to Railway or Modal.#What to Build Next
Once your basic chatbot works, these are the upgrades that turn it from "tutorial output" into "interview-worthy portfolio piece."
#An Eight-Week Plan
Here is a realistic schedule for taking this lesson from a CLI script to something worth showing. It is a plan to follow, not a promise of any particular outcome:
- Day 1-2: build the CLI version (this lesson).
- Day 3-4: Streamlit and a deploy.
- Week 2: add RAG over a document set you are allowed to use. If it belongs to your employer, get explicit sign-off first.
- Week 3: demo it to a real audience, such as an internal hackathon or a team meeting.
- Week 4: write it up publicly, with anything confidential stripped out.
Budget roughly 400 lines of code including the Streamlit app and the eval harness, and roughly 50 hours spread over eight weeks of nights and weekends. What makes this worth doing is not the traffic the write-up gets; it is that you end up with a working artifact you can open in an interview and explain line by line.
#Quick Check
How does an LLM 'remember' the conversation from message #1 when it answers message #10?
Key Takeaways
- LLMs are stateless — they remember nothing between calls. 'Memory' is the trick of sending the full history each turn.
- Conversation memory is a list of messages sent with every API call. The list grows; you pay tokens for the full list each turn.
- The system prompt controls personality and specialty — your differentiator vs. a thousand generic chatbots.
- Use a sliding window (last 40 messages) and summarize older turns to control cost at scale.
- Streaming is mandatory in 2026 — 5 lines, 60-80% perceived-latency drop.
- Deploy to Streamlit Cloud in 20 minutes. The live URL multiplies the portfolio value 5x.