Build a Multi-Turn Chatbot
Build a Streamlit chatbot that remembers conversations with OpenRouter.
Introduction
30 Second Summary
A chatbot can sound attentive while forgetting what you said one message ago. That gap becomes obvious when your follow-up depends on an earlier detail.
In this project, you will build Context Keeper, a browser-based chatbot written in Python that sends prompts through OpenRouter. The app gains reliable memory by resending its stored conversation history with every request.
What You'll Build
Your finished chatbot recalls cobalt-17 from an earlier message when you ask for the code word in a follow-up.
By the end of this project, you'll have:
- A browser-based chat experience in Streamlit that displays each OpenRouter reply with its selected free model.
- Reliable multi-turn memory that recalls the exact code word from the complete ordered transcript.
- A fresh-start control that clears the conversation context with one click.
- Secret Mission: Add an editable system message that turns Context Keeper into a focused Python study coach.
Are there any prerequisites?
The required environment is a Windows computer running Visual Studio Code with internet access. Basic Python scripting is enough because no previous API experience is required.
Before We Start
Before any setup, lock in what Context Keeper will remember. Connect that memory to the ordered conversation history the app resends with each stateless chat-completion request.
Set Up Secure OpenRouter Access
Context Keeper needs Python packages plus OpenRouter access before it can send a chat request. A clean environment keeps those packages separate from your other Python projects.
This step also keeps your API key outside the Python source file. Protecting that credential now prevents accidental exposure when you work with the chatbot later.
In this step, get ready to:
- Confirm that your installed Python version supports the pinned packages.
- Create an isolated project environment with Streamlit and Requests.
- Protect your OpenRouter API key and verify the Streamlit installation.
Check your Python version
Streamlit 1.66.0 supports Python 3.11 through 3.15. Checking your installed version now prevents dependency errors during setup.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search box.
- Press Enter to open Visual Studio Code.
- Select View from the menu bar.
- Select Terminal to open the integrated terminal.
- Select PowerShell from the terminal profile dropdown if another shell opens.
- Check your installed Python version by running this command:
python --version
What does this command check?
The command prints the Python version used by the python command in this terminal. Use the matching tab below for the result you see.
✔️ I see a supported version
Your output shows Python 3.11 through 3.15. Your interpreter is ready for the pinned packages.
ⓧ I see an older version
Your current interpreter is below the supported range. Install Python 3.15.0 before creating the project environment.
- Open the official Python Windows downloads page in your browser.
- Download the current Python 3.15.0 Windows release.
- Run the downloaded installer.
- Enable Add Python to PATH during installation.
- Close Visual Studio Code after the installation finishes.
- Reopen Visual Studio Code through Windows search.
- Return to the integrated PowerShell terminal.
- Check the updated version by running:
python --version
What should the new check show?
The output should now report Python 3.15.0. This confirms the supported interpreter is available through the python command.
ⓧ Command not found
Windows cannot currently find Python from your terminal. Installing Python 3.15.0 with its path option enabled makes the command available.
- Open the official Python Windows downloads page in your browser.
- Download the current Python 3.15.0 Windows release.
- Run the downloaded installer.
- Enable Add Python to PATH during installation.
- Close Visual Studio Code after the installation finishes.
- Reopen Visual Studio Code through Windows search.
- Return to the integrated PowerShell terminal.
- Confirm that Windows can now find Python by running:
python --version
What should the command show?
The output should report Python 3.15.0. The command is now available to your PowerShell environment.
Create the isolated project environment
A virtual environment gives the project its own Python package space. The pinned dependency file makes the same package versions available every time you rebuild that environment.
- Select File from the Visual Studio Code menu bar.
- Select Open Folder....
- Select your Desktop in the folder dialog.
- Select New Folder.
- Enter context-keeper as the folder name.
- Select Select Folder to open the new folder.
- Select Yes, I trust the authors if Visual Studio Code shows a Workspace Trust prompt for the folder you created.
You should see context-keeper at the top of the Explorer sidebar. This confirms that every file you create next belongs to the project.
- Select New File... in the Explorer toolbar.
- Type requirements.txt as the filename.
- Press Enter to create the file.
- Add the pinned project dependencies by pasting this code into requirements.txt:
streamlit==1.66.0
requests==2.34.2
Why pin these packages?
- Streamlit 1.66.0 provides the browser interface you use for the chatbot.
- Requests 2.34.2 sends the authenticated HTTP request to OpenRouter.
- Exact pins keep your environment aligned with the code in this project.
- Press Ctrl+S to save requirements.txt.
- Confirm that requirements.txt appears beneath context-keeper in the Explorer sidebar.
Missing requirements.txt?
Check that context-keeper is the folder shown at the top of Explorer. Recreate the file inside that folder if it opened somewhere else.
Ask for help if the file still does not appear. Help me create requirements.txt in my context-keeper folder in Visual Studio Code.
The dependency installation can take a minute while pip downloads both packages. A steady stream of package names in the terminal means the installation is progressing.
- Select Terminal from the Visual Studio Code menu bar.
- Select New Terminal.
- Select PowerShell from the terminal profile dropdown if another shell opens.
- Create the virtual environment and install the dependencies by running these commands:
python -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
What do these commands create?
- The first command creates an isolated Python environment in .venv.
- The second command activates that environment in the current PowerShell terminal.
- The third command installs the exact package versions stored in requirements.txt.
- Confirm that the PowerShell prompt begins with (.venv) after activation.
- Confirm that the installation output reports successful installs for Streamlit 1.66.0 plus Requests 2.34.2.
Environment command failed?
If activation is blocked by your PowerShell policy, keep the error visible while you ask for help. If the requirements file is missing, confirm that the terminal path ends with context-keeper.
Check that all three commands ran in order. Help me debug my context-keeper virtual environment setup in Windows PowerShell.
Protect your OpenRouter API key and verify Streamlit
Your OpenRouter key authorizes requests from Context Keeper. Treat it like a password because anyone who obtains it can use your account's API access.
- Open the OpenRouter API keys page in your browser.
- Create an OpenRouter account through the page's sign-up flow.
- Create a new API key through the key management controls.
- Copy the new key while it is available.
Credential setup can feel risky. The key stays in a local secrets file that your ignore rules explicitly protect from source control.
- Return to the context-keeper folder in Visual Studio Code.
- Right-click context-keeper in the Explorer sidebar.
- Select New Folder.
- Enter .streamlit as the folder name.
- Press Enter to create the folder.
- Confirm that .streamlit appears beneath context-keeper in Explorer.
- Right-click the new .streamlit folder.
- Select New File....
- Create the secrets file by naming it secrets.toml.
- Add the OpenRouter secret template by pasting this code:
OPENROUTER_API_KEY = "your-api-key-here"
How does the secret stay outside your code?
Streamlit reads project secrets from .streamlit/secrets.toml. Later code accesses the value by its OPENROUTER_API_KEY name without placing the credential inside app.py.
- Replace your-api-key-here with the OpenRouter key you copied.
- Press Ctrl+S to save .streamlit/secrets.toml.
- Close the editor tab containing the visible key after saving it.
Secret is not loading?
Confirm that the file path is exactly .streamlit/secrets.toml. Restart the Streamlit server whenever you create or edit the secrets file while a server is running.
Keep the key private when requesting support. Help me check my Streamlit secrets file without sharing my API key.
- Right-click context-keeper in the Explorer sidebar.
- Select New File....
- Create the ignore file by naming it .gitignore.
- Protect the virtual environment and secrets file by pasting this code:
.venv/
.streamlit/secrets.toml
__pycache__/
What does this file protect?
The .gitignore file excludes the local virtual environment plus the secrets file from future Git tracking. It also excludes Python's generated __pycache__ folders.
- Press Ctrl+S to save .gitignore.
- Confirm that Explorer lists .gitignore beside requirements.txt.
Ignore file missing?
Check that the filename begins with a dot. Confirm that it has no extra .txt extension.
Ask for help before moving on if the name keeps changing. Help me create a correctly named .gitignore file in Visual Studio Code.
✔️ Awesome, I've got everything!
Great. Save every file before testing Streamlit.
ⓧ I'd like to double check the full code
Compare each project file with the references below. The placeholder in the secrets reference represents your private key.
streamlit==1.66.0
requests==2.34.2
What should match?
Your requirements.txt file should contain these two pinned dependencies in this order.
.venv/
.streamlit/secrets.toml
__pycache__/
What should match?
Your .gitignore file should contain these three protected paths with no extra characters.
OPENROUTER_API_KEY = "your-api-key-here"
What should differ locally?
Your local secrets file should use this exact key name. Its quoted value should be your real OpenRouter key instead of the displayed placeholder.
Before you run the final check, do you expect the Streamlit example app to open from the activated environment?
- Return to the PowerShell terminal whose prompt begins with (.venv).
- Continue through any optional first-run setup prompt without entering personal information.
- Verify the Streamlit installation by running this command:
python -m streamlit hello
What does this command prove?
The command starts Streamlit from the active virtual environment. It serves the included example app so you can verify that the installation works in a browser.
Your browser should open the Streamlit example app. The terminal should remain active while the example app is running.
Streamlit app did not open?
If the terminal prints a local address without opening a browser, copy that address into your browser. If Streamlit cannot be found, confirm that the prompt begins with (.venv).
Return to the dependency installation earlier in this step if the packages are missing. Help me debug why python -m streamlit hello is not opening the example app.
- Return to the PowerShell terminal after capturing your screenshot.
- Press Ctrl+C to stop the Streamlit example app.
Your secure project environment is ready. Next up, you will make the first OpenRouter request through a chatbot that reveals exactly what happens when conversation history is missing.
Build a Chatbot That Forgets
Your Streamlit setup can already serve a browser app. Now it needs a chat interface that sends a real prompt to OpenRouter.
A one-message chatbot gives you a working chat-completion request early. The follow-up test then exposes the exact gap that conversation history solves.
In this step, get ready to:
- Create a browser-based chat interface.
- Send the current prompt through an authenticated API request.
- Test whether a follow-up can recover an earlier fact.
Create the first chat interface
A chat interface needs an input for the learner's message. It also needs separate containers that visually distinguish the user from the assistant.
- Create app.py inside the context-keeper folder with the following code by using the Explorer sidebar in Visual Studio Code:
import requests
import streamlit as st
API_URL = "https://openrouter.ai/api/v1/chat/completions"
MODEL = "openrouter/free"
st.title("Context Keeper")
st.write("A chatbot powered by OpenRouter's free model router.")
api_key = st.secrets["OPENROUTER_API_KEY"]
if prompt := st.chat_input("Tell me something to remember"):
with st.chat_message("user"):
st.markdown(prompt)
What does this code do?
- The requests import prepares the app to send an HTTP request.
- The streamlit import provides the browser interface.
- API_URL stores the chat-completions endpoint.
- MODEL selects the free model router.
- st.secrets reads the protected API key from the secrets file you created earlier.
- st.chat_input collects the current prompt.
- st.chat_message displays that prompt inside a user message container.
- Save app.py.
- Return to the activated PowerShell terminal from earlier.
- Start the chatbot by running this command:
python -m streamlit run app.py
What does this command do?
This command asks Python to run app.py through Streamlit. It starts a local app server.
The server keeps using this terminal while the app runs. Your browser opens the chat interface served by that process.
- Switch to the browser window opened by Streamlit.
You should see the Context Keeper title. You should also see a chat input labelled Tell me something to remember.
Chat interface not opening?
- Confirm that app.py sits directly inside the context-keeper folder.
- Confirm that the PowerShell prompt still begins with (.venv).
- Check the terminal for the local address that Streamlit provides.
Help me debug why my Streamlit chat interface is not opening.
Send one prompt to OpenRouter
The interface can collect a prompt. The next code turns that prompt into an authenticated request with a JSON payload.
- In app.py, place the following code directly after the indented st.markdown(prompt) line:
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
}
response = requests.post(API_URL, headers=headers, json=payload)
data = response.json()
reply = data["choices"][0]["message"]["content"]
model_used = data["model"]
with st.chat_message("assistant"):
st.markdown(reply)
st.write(f"Model used: `{model_used}`")
How does the request work?
- headers carries the API key through Bearer authorization.
- payload names the free router.
- messages contains one user message built from the current prompt.
- requests.post sends the payload to the endpoint.
- data holds the decoded response.
- reply extracts the assistant's text.
- model_used records the free model selected for this response.
- Save app.py.
- Return to the chatbot in your browser.
The free router can respond more slowly at peak times. A short pause after submitting your message is normal.
- Enter My project code word is cobalt-17. in the chat input.
- Submit the message by pressing Enter.
You should see your message followed by an assistant response. A Model used: line identifies the free model selected for the request.
That is your first live chatbot exchange. Context Keeper can now send a prompt to OpenRouter and render the returned answer.
No assistant response?
- Confirm that your API key remains saved under OPENROUTER_API_KEY in .streamlit/secrets.toml.
- Restart the Streamlit server if you edited the secrets file while it was running.
- Try the request again if a free model is temporarily unavailable.
Help me diagnose why my OpenRouter request is not returning an assistant response.
✔️ Awesome, I've got everything!
Your working chatbot now sends one prompt to OpenRouter. Double check that app.py is saved before continuing.
ⓧ I'd like to double check the full code
Your complete app.py file should match this reference exactly.
import requests
import streamlit as st
API_URL = "https://openrouter.ai/api/v1/chat/completions"
MODEL = "openrouter/free"
st.title("Context Keeper")
st.write("A chatbot powered by OpenRouter's free model router.")
api_key = st.secrets["OPENROUTER_API_KEY"]
if prompt := st.chat_input("Tell me something to remember"):
with st.chat_message("user"):
st.markdown(prompt)
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
}
response = requests.post(API_URL, headers=headers, json=payload)
data = response.json()
reply = data["choices"][0]["message"]["content"]
model_used = data["model"]
with st.chat_message("assistant"):
st.markdown(reply)
st.write(f"Model used: `{model_used}`")
Experience the missing-context problem
Your first message established a fact that only appeared in that request. The next check asks a follow-up that depends on the earlier turn.
Before you submit the follow-up, decide whether you expect an exact answer or an uncertain response.
- Enter What is my project code word? in the chat input.
- Submit the follow-up by pressing Enter.
You will see a response that lacks dependable recall of cobalt-17. The assistant may say it lacks the earlier context or offer a guess.
- Switch back to app.py in Visual Studio Code.
- Find this entry inside payload:
"messages": [{"role": "user", "content": prompt}],
Why did the follow-up forget?
The API receives only the messages included in payload. This line sends the newest prompt as a single user message.
The earlier user message never enters the follow-up request. Reliable recall requires the app to resend the complete ordered conversation history.
You have now watched a working chatbot lose context between requests. Next, you will use Streamlit Session State to store both sides of each turn and resend the complete history.
Give the Chatbot Conversation Memory
Your chatbot now gets replies from OpenRouter. The code-word test exposed the missing piece: the first turn vanished before the follow-up request.
This step uses Streamlit Session State to preserve each message through reruns. Each request then carries the complete ordered history.
In this step, get ready to:
- Preserve every chat message in Session State.
- Replay stored user and assistant turns in the browser.
- Send ordered history to OpenRouter to recover cobalt-17.
Store history in Session State
Streamlit reruns app.py after each chat interaction. Session State keeps the message list available during those reruns.
- In app.py, locate the block that begins with st.title("Context Keeper").
- Replace that block through the api_key assignment with this code:
st.title("Context Keeper")
st.write("A multi-turn chatbot powered by OpenRouter's free model router.")
api_key = st.secrets["OPENROUTER_API_KEY"]
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
How does the replay loop work?
- The updated description identifies the app as a multi-turn chatbot.
- The initialization creates st.session_state.messages only when the list does not already exist.
- The loop reads each stored role to choose the chat container.
- The loop renders each stored content value inside that container.
- Save app.py.
- Switch back to the browser app from earlier.
You should see A multi-turn chatbot powered by OpenRouter's free model router. above the chat input. The input should still accept messages.
Does the app stop rendering?
Check that the if block aligns with the for loop. Keep the two lines inside each block indented by four spaces.
Ask for help if the state initialization prevents the page from loading. Help me debug my Session State initialization and replay loop.
Save both sides of each turn
A complete conversation needs the user's message plus the assistant's reply. Their order shows the API who said each piece of content.
- In app.py, locate the line that begins with if prompt :=.
- Add the user message to the history directly below that line by pasting:
st.session_state.messages.append({"role": "user", "content": prompt})
Why append the user first?
The user's newest message must enter the ordered history before the API request. Its user role identifies the speaker.
- Locate model_used = data["model"] farther down in app.py.
- Add the assistant reply to the history directly below that line by pasting:
st.session_state.messages.append(
{"role": "assistant", "content": reply}
)
Why store the assistant reply?
The parsed reply joins the same message list after OpenRouter responds. Its assistant role preserves the other side of the conversation.
- Save app.py.
- Enter Reply with one word: stored. in the chat input.
You should see your message plus the assistant response as two chat bubbles. Both sides of that turn now exist in st.session_state.messages.
Is a turn missing from the transcript?
Place the user append directly inside the prompt block. Place the assistant append immediately after model_used is assigned.
Check that both dictionaries use the exact keys role plus content.
Get help tracing a missing chat turn. Help me find why one side of my conversation is missing from Session State.
Send the complete message list
The payload still creates a one-item message list from the newest prompt. That line leaves the stored history outside the request.
- In app.py, locate the messages entry inside payload.
- Replace that entry with the complete Session State list by pasting:
"messages": st.session_state.messages,
Why does this preserve the conversation?
Every request now includes the ordered user and assistant dictionaries stored during earlier turns. The model receives the context required to answer a follow-up.
- Save app.py.
✔️ Awesome, I've got everything!
Your message history now survives Streamlit reruns. Confirm that app.py is saved before testing the complete conversation.
ⓧ I'd like to double check the full code
- Compare your saved app.py with this complete version:
import requests
import streamlit as st
API_URL = "https://openrouter.ai/api/v1/chat/completions"
MODEL = "openrouter/free"
st.title("Context Keeper")
st.write("A multi-turn chatbot powered by OpenRouter's free model router.")
api_key = st.secrets["OPENROUTER_API_KEY"]
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if prompt := st.chat_input("Tell me something to remember"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {
"model": MODEL,
"messages": st.session_state.messages,
}
response = requests.post(API_URL, headers=headers, json=payload)
data = response.json()
reply = data["choices"][0]["message"]["content"]
model_used = data["model"]
st.session_state.messages.append(
{"role": "assistant", "content": reply}
)
with st.chat_message("assistant"):
st.markdown(reply)
st.write(f"Model used: `{model_used}`")
- If the app has stopped, restart it from the activated .venv PowerShell terminal by running:
python -m streamlit run app.py
What does this command do?
This command runs app.py as a Streamlit app. The chatbot opens in your browser for the final memory test.
Does the app fail to start?
Confirm that the activated virtual environment is still selected in PowerShell. Check that app.py is saved inside the context-keeper folder.
Get help with the launch command. Help me debug why my Streamlit app does not start from my activated PowerShell environment.
Before you test the follow-up, do you expect the complete message list to contain enough context for exact recall?
- Enter My project code word is cobalt-17. in the chat input.
- Enter What is my project code word? as the next message.
You should see the assistant return cobalt-17 in the follow-up response. The complete ordered history is now reaching OpenRouter.
Does the chatbot still forget?
Confirm that the payload points to st.session_state.messages. A fresh one-item list still excludes earlier turns.
Check that the user message is appended before the request. Check that the assistant reply is appended after a successful response.
Ask for help tracing the request history. Help me debug why my chatbot still forgets cobalt-17.
That's the memory gap closed: your chatbot now carries its conversation forward between turns. Next, you'll add reset controls plus clearer recovery when a request cannot succeed.
Add Reset and Recovery Controls
Your Streamlit chatbot now preserves ordered history through Session State. A missing key or failed request can still leave the learner without a clear recovery path.
A dedicated Requests function gives each OpenRouter call a timeout. Clear feedback will also explain failures before they interrupt the conversation.
In this step, get ready to:
- Move the API request into a reusable function with error handling.
- Show recovery feedback when API access fails.
- Reset the stored conversation with a clear-chat control.
Isolate the API request
The current secret lookup stops the entire app when the secrets file or key is missing. A guarded lookup lets the interface load with recovery guidance.
- In app.py, find api_key = st.secrets["OPENROUTER_API_KEY"].
- Replace that line with the guarded secret lookup below:
try:
api_key = st.secrets["OPENROUTER_API_KEY"]
except (FileNotFoundError, KeyError):
api_key = None
What does this code do?
- The try branch reads the saved API key during normal startup.
- The except branch catches a missing secrets file or key.
- The None value gives the interface a safe failure state to handle later.
- Save app.py.
- Stop the current server by pressing Ctrl+C if it is still running.
- Check that the guarded lookup still loads the app by running:
python -m streamlit run app.py
What does this check prove?
This command reloads app.py with the valid secret from earlier. It keeps the PowerShell terminal busy until you stop the server.
- Submit Tell me one fact about Python lists. through the chat input.
You should see an assistant response with the selected model underneath it. The guarded secret lookup is working without changing the successful chat path.
App Stops During Startup?
Check that try starts at the left edge of app.py. Confirm that api_key = None is indented beneath except.
If the traceback remains, help me debug the guarded secret lookup in my Streamlit app.
- Stop the server by pressing Ctrl+C in PowerShell.
The API request also needs one boundary for timeouts and response checks. Moving that work into call_openrouter() keeps the chat flow focused on conversation state.
- In app.py, place the following function directly below the guarded secret lookup:
def call_openrouter(messages):
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {
"model": MODEL,
"messages": messages,
}
try:
response = requests.post(
API_URL,
headers=headers,
json=payload,
timeout=30,
)
response.raise_for_status()
data = response.json()
return data["choices"][0]["message"]["content"], data["model"]
except requests.exceptions.RequestException as error:
st.error(f"OpenRouter request failed: {error}")
except (KeyError, IndexError, TypeError) as error:
st.error(f"OpenRouter returned an unexpected response: {error}")
return None, None
How does the request function recover?
- The messages parameter carries the complete ordered conversation into the request payload.
- The timeout=30 setting prevents the request from waiting indefinitely for response data.
- The raise_for_status() call turns an unsuccessful status into a handled request exception.
- The exception branches display separate feedback for request failures or malformed response data.
- The final return provides a predictable failure signal when no usable reply arrives.
- Save app.py.
- Check that the new function has valid syntax by running:
python -m streamlit run app.py
What does this run check?
Python reads the complete function before Streamlit renders the interface. A loaded chat page confirms that the new function has valid syntax.
- Submit What fact did I ask about? through the chat input.
You should receive another response because the existing inline request still handles this exchange. The next change connects the new recovery function to the chat flow.
Function Preventing Startup?
Confirm that def call_openrouter(messages): begins at the left edge of the file. Check that both except branches align with the function's try branch.
If the function still blocks startup, help me trace the syntax problem in call_openrouter().
- Stop the server by pressing Ctrl+C in PowerShell.
Add visible recovery feedback
The request function now returns both the assistant content and selected model after a successful call. The surrounding chat flow must preserve only exchanges that reached that successful path.
- In app.py, find the block that starts with if prompt := st.chat_input("Tell me something to remember"):.
- Replace that block through the end of the file with the recovery-aware chat flow below:
if not api_key:
st.error(
"Add OPENROUTER_API_KEY to .streamlit/secrets.toml, then restart the app."
)
elif prompt := st.chat_input("Tell me something to remember"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
reply, model_used = call_openrouter(st.session_state.messages)
if reply is not None:
st.session_state.messages.append(
{"role": "assistant", "content": reply}
)
with st.chat_message("assistant"):
st.markdown(reply)
st.write(f"Model used: `{model_used}`")
else:
st.session_state.messages.pop()
How does the chat flow respond?
- The first branch displays missing-key guidance before the learner can submit a message.
- The success branch stores the assistant response after the API call returns usable content.
- The selected model appears beneath every successful assistant response.
- The failure branch removes the unmatched user message from stored history.
- Save app.py.
- Start the recovery-aware app by running:
python -m streamlit run app.py
What does this run activate?
The chat flow now sends its stored history through call_openrouter(). Successful responses return both the assistant content and the selected free model.
- Submit Explain a Python dictionary in one sentence. through the chat input.
You should see an assistant response with a model identifier beneath it. That confirms the successful request path now uses the isolated function.
Good progress. Context Keeper now surfaces request problems inside the browser while keeping failed turns out of its stored history.
No Reply Appearing?
Confirm that reply, model_used = call_openrouter(st.session_state.messages) is inside the chat-input branch. Check that the success and failure branches align beneath that function call.
If the in-app error remains unclear, help me debug the recovery-aware chat flow.
Add a clean reset
A fresh conversation needs an empty stored history before the replay loop renders old messages. The reset control replaces the existing message list with a new empty list.
- In app.py, find the message initialization block and the replay loop beneath it.
- Replace that section with the reset-aware version below:
if "messages" not in st.session_state:
st.session_state.messages = []
if st.button("Clear chat"):
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
How does the reset work?
- The initialization block creates the history list only when the current session does not have one.
- The Clear chat button replaces stored history with an empty list when clicked.
- The replay loop runs after the reset check, so cleared messages disappear from the transcript.
- Save app.py.
- Stop the running server by pressing Ctrl+C in PowerShell.
Before you run the completed app, do you expect clearing the visible transcript to remove its stored context too?
- Start the completed app by running:
python -m streamlit run app.py
What does the final run load?
This run loads the reset control with the recovery-aware request flow. The app remains connected to the valid secret from earlier.
- Submit My temporary reset word is amber. through the chat input.
You should see the completed exchange in the transcript. The selected model should appear beneath the assistant response.
Before you click the reset control, which messages do you expect to remain visible?
- Click Clear chat above the transcript.
You should see the complete transcript disappear. The empty chat input remains ready for a conversation with no previous context.
Transcript Still Visible?
Confirm that the button block appears before the replay loop. Check that it assigns an empty list to st.session_state.messages.
If old messages remain, help me debug why Clear chat does not reset Session State.
The final recovery test temporarily changes only the secret's name. Your actual API key value stays inside the private secrets file.
- Switch back to .streamlit/secrets.toml in the editor from earlier.
- Rename OPENROUTER_API_KEY to OPENROUTER_API_KEY_TEST.
- Save .streamlit/secrets.toml.
- Stop the running server by pressing Ctrl+C in PowerShell.
Before you restart the app, do you expect it to stop or remain open with recovery guidance?
- Restart the app with the temporarily renamed secret by running:
python -m streamlit run app.py
What does the missing-key test reveal?
Restarting makes Streamlit read the updated secrets file. You should see Add OPENROUTER_API_KEY to .streamlit/secrets.toml, then restart the app. inside the browser.
The interface remains open because the guarded lookup converts the missing key into a recovery state.
Recovery Message Not Showing?
Confirm that the temporary secret name is OPENROUTER_API_KEY_TEST. Restart the server after saving because the changed secrets file must be loaded again.
If the old key still appears active, help me test the missing-secret path safely.
- Switch back to .streamlit/secrets.toml.
- Restore the secret name to OPENROUTER_API_KEY.
- Save .streamlit/secrets.toml.
- Stop the running server by pressing Ctrl+C in PowerShell.
- Reload the restored secret by running:
python -m streamlit run app.py
What should return?
The restored name lets the guarded lookup read your valid key again. You should see the chat input return beneath the empty transcript.
That completes the recovery loop. Context Keeper can now reset its memory, explain missing credentials, preserve successful exchanges, and surface request failures.
✔️ Awesome, I've got everything!
Your saved app.py now includes request recovery, model visibility, missing-key guidance, and a clear-chat reset.
ⓧ I'd like to double check the full code
Compare your saved app.py with this completed version:
import requests
import streamlit as st
API_URL = "https://openrouter.ai/api/v1/chat/completions"
MODEL = "openrouter/free"
st.title("Context Keeper")
st.write("A multi-turn chatbot powered by OpenRouter's free model router.")
try:
api_key = st.secrets["OPENROUTER_API_KEY"]
except (FileNotFoundError, KeyError):
api_key = None
def call_openrouter(messages):
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {
"model": MODEL,
"messages": messages,
}
try:
response = requests.post(
API_URL,
headers=headers,
json=payload,
timeout=30,
)
response.raise_for_status()
data = response.json()
return data["choices"][0]["message"]["content"], data["model"]
except requests.exceptions.RequestException as error:
st.error(f"OpenRouter request failed: {error}")
except (KeyError, IndexError, TypeError) as error:
st.error(f"OpenRouter returned an unexpected response: {error}")
return None, None
if "messages" not in st.session_state:
st.session_state.messages = []
if st.button("Clear chat"):
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if not api_key:
st.error(
"Add OPENROUTER_API_KEY to .streamlit/secrets.toml, then restart the app."
)
elif prompt := st.chat_input("Tell me something to remember"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
reply, model_used = call_openrouter(st.session_state.messages)
if reply is not None:
st.session_state.messages.append(
{"role": "assistant", "content": reply}
)
with st.chat_message("assistant"):
st.markdown(reply)
st.write(f"Model used: `{model_used}`")
else:
st.session_state.messages.pop()
What should match?
This reference is the completed app.py for Context Keeper. Your requirements.txt, .gitignore, and .streamlit/secrets.toml remain unchanged.
Secret mission
Turn Context Keeper into a Python Study Coach
Give Context Keeper a focused teaching role through an editable system instruction. You will change its teaching style while preserving the conversation history that makes follow-up questions work.
Clean Up Your Resources
Clean Up Your Resources
Context Keeper uses OpenRouter's Free Models Router. Requests routed to free models have no charge.
Choose whether to keep the project ready, pause the Streamlit server, or delete the local files plus the API key.
Resources you used:
- The context-keeper project folder containing your app, pinned dependencies, and virtual environment.
- The .streamlit/secrets.toml file containing your OpenRouter API key.
- The OpenRouter API key managed through your account.
- The Streamlit server process in your activated .venv PowerShell environment.
Keep everything running
No action is needed. Choose this if you plan to keep experimenting with the Python study coach.
- Keep the context-keeper folder in its current location.
- Keep .streamlit/secrets.toml private because it contains an active API key.
- Leave the Streamlit server running only while you are using the app.
Pause - I'll come back to this later
Stop the local Streamlit process to free the PowerShell terminal. Your project files plus API key remain available for your next session.
- Return to the PowerShell terminal from earlier.
- Press Ctrl+C to stop Streamlit.
- Confirm the PowerShell prompt returns with (.venv) at the beginning.
- Restart the study coach later by running the existing Streamlit app command from the activated environment.
Delete - I don't want to use this again
Removing the study coach can feel final. The API key is revoked before the folder goes so the stored credential cannot be used again.
Revoke the API Key
- Open the OpenRouter API key page in your browser.
- Select the API key you created for Context Keeper.
- Revoke the selected API key.
- Confirm the page shows that the revocation completed.
The credential stored in .streamlit/secrets.toml can no longer authorize requests once its revocation completes.
Remove the Local Project
- Return to the PowerShell terminal from earlier.
- Press Ctrl+C if Streamlit is still running.
- Close Visual Studio Code to release the project folder.
Your editor plus local server are now out of the way. Windows can remove the complete project folder.
- Press the Windows key to open the search bar.
- Type File Explorer. Press Enter to open it.
- Locate the context-keeper folder you created in Step 1.
- Right-click the context-keeper folder.
- Select Delete from the menu.
- Confirm that context-keeper no longer appears in its original location.
Nice Work!
Nice Work!
You did it! Context Keeper now runs as a browser-based Python study coach through Streamlit. It calls OpenRouter while your app controls the full conversation history.
You've learned how to:
- Build a browser-based Python chatbot that sends authenticated JSON requests to OpenRouter with Requests.
- Expose the missing-context problem by sending only the newest prompt. Fix that gap with ordered user and assistant history stored in Streamlit Session State.
- Make the app safer with protected secrets, clear-chat behavior, request timeouts, friendly failures, and selected-model visibility.
- Complete the Secret Mission by adding an editable system instruction that turns Context Keeper into a Python study coach without storing the instruction as a normal chat turn.
Ready to quiz yourself?