A conversational AI chatbot built with Streamlit and the Groq API, featuring persistent multi-turn memory, a custom persona, and production-style error handling.
This is Project 4 of a 12-month self-directed AI Engineering roadmap — built to understand why AI-powered apps are structured the way they are, not just to produce working code.
- 💬 Multi-turn conversation with real memory (the model recalls earlier context in the same session)
- ⚡ Powered by Groq's
llama-3.3-70b-versatile— fast, free-tier inference - 🧠 Custom system prompt defining a consistent assistant persona
- 🛡️ Graceful error handling for auth failures, rate limits, and connection issues — no raw tracebacks shown to the user
- 🧹 "Clear Chat" button that resets conversation while preserving the assistant's persona
- 🏗️ Clean separation between UI logic and API logic
| Tool | Purpose |
|---|---|
| Python | Core language |
| Streamlit | Web UI, no HTML/CSS/JS required |
Groq API (groq SDK) |
LLM inference |
| python-dotenv | Secure API key management |
User (browser)
↓
Streamlit UI (chat_input, chat_message)
↓
st.session_state.messages (persists across reruns)
↓
groq_client.py → Groq API (LLM)
↓
Response appended to session_state → UI redraws full history
Why this shape matters: Streamlit reruns the entire script on every user interaction. Without st.session_state, conversation history would reset on every message. The LLM API itself is stateless — it has no memory between calls — so the entire message history is resent on every request to simulate memory.
ai-chatbot/0-api-based/
├── app.py # Streamlit UI — orchestration layer
├── groq_client.py # Groq API logic — business logic layer
├── .env # GROQ_API_KEY (not committed)
├── .gitignore
├── requirements.txt
└── README.md
-
Clone the repo and navigate into the project folder:
git clone <your-repo-url> cd ai-chatbot/0-api-based
-
Create and activate a virtual environment:
python -m venv venv venv\Scripts\activate # Windows
-
Install dependencies:
pip install -r requirements.txt
-
Create a
.envfile in the project root:GROQ_API_KEY=your_api_key_here -
Run the app:
streamlit run app.py
Why is groq_client.py separated from app.py?
groq_client.py is the business logic layer — it has no idea whether it's being called from a Streamlit app, a CLI script, or a test suite. app.py is the orchestration layer that decides how to display things. This mirrors the same separation used in earlier projects (e.g., expense_manager.py vs main.py), and means swapping Groq for another provider later only requires touching one file.
Why lazy initialization for the Groq client?
Creating the client at import time causes side effects (reading .env, instantiating a client) the moment the module is imported — even if it's never actually used. Recreating the client on every single call is wasteful. Lazy initialization creates the client once, on first use, and reuses it afterward — the same pattern Streamlit itself uses internally via @st.cache_resource.
Why are exceptions caught in app.py, not groq_client.py?
groq_client.py doesn't know how it's being consumed, so it shouldn't decide how errors are presented. It lets exceptions propagate. app.py knows it's a Streamlit UI, so it's responsible for catching specific exceptions (AuthenticationError, RateLimitError, APIConnectionError) and showing a user-friendly message instead of a raw traceback.
Why isn't the error fallback message saved into session_state.messages?
The entire message list is resent to the LLM on every call. If a fallback error string like "Unable to connect" were saved into that list, it would later be sent back to the model as if it were part of the real conversation — potentially confusing the model or causing it to reference an error that a human never actually said. Error messages are shown to the user but excluded from the API-facing conversation history.
- How Streamlit's rerun model works, and why
st.session_stateis non-negotiable for any persistent UI - Why LLM APIs are stateless, and how conversation memory is actually simulated (resending full history)
- The lazy initialization pattern, and where it's the right middle ground between "too eager" and "too wasteful"
- Layered exception handling: catching specific exception types before generic ones, and deciding where in an app errors should be caught vs. where they should propagate
- Why UI-facing error messages must be kept separate from the data a model actually sees
Future iterations planned in this project family:
1-with-rag/— document-grounded chatbot using retrieval-augmented generation2-with-agents/— tool-calling and agentic behavior
Part of a 12-month AI Engineering self-development roadmap — Month 2, Project 4.